diff --git a/guacamole/src/licenses/bundled/glyptodon-enterprise-player/LICENSE b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/LICENSE
new file mode 100644
index 000000000..69e97671e
--- /dev/null
+++ b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2019 Glyptodon, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/guacamole/src/licenses/bundled/glyptodon-enterprise-player/README b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/README
new file mode 100644
index 000000000..c9cef3005
--- /dev/null
+++ b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/README
@@ -0,0 +1,9 @@
+Session Recording Player for Glyptodon Enterprise
+(https://github.com/glyptodon/glyptodon-enterprise-player)
+----------------------------------------------------------
+
+ Version: 1.1.0-1
+ From: 'Glyptodon, Inc.' (https://glyptodon.com/)
+ License(s):
+ MIT (bundled/glyptodon-enterprise-player/LICENSE)
+
diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js
index 38ef98a5c..d1e78754c 100644
--- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js
+++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js
@@ -180,6 +180,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
resolve : { updateCurrentToken: updateCurrentToken }
})
+ // Recording player
+ .when('/settings/:dataSource/recording/:identifier/:name', {
+ title : 'APP.NAME',
+ bodyClassName : 'settings',
+ templateUrl : 'app/settings/templates/settingsConnectionHistoryPlayer.html',
+ controller : 'connectionHistoryPlayerController',
+ resolve : { updateCurrentToken: updateCurrentToken }
+ })
+
// Client view
.when('/client/:id', {
bodyClassName : 'client',
diff --git a/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js b/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js
new file mode 100644
index 000000000..ccf8ad749
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Filter which accepts a promise as input, returning the resolved value of
+ * that promise if/when the promise is resolved. While the promise is not
+ * resolved, null is returned.
+ */
+angular.module('index').filter('resolve', [function resolveFilter() {
+
+ /**
+ * The name of the property to use to store the resolved promise value.
+ *
+ * @type {!string}
+ */
+ var RESOLVED_VALUE_KEY = '_guac_resolveFilter_resolvedValue';
+
+ return function resolveFilter(promise) {
+
+ if (!promise)
+ return null;
+
+ // Assign value to RESOLVED_VALUE_KEY automatically upon resolution of
+ // the received promise
+ if (!(RESOLVED_VALUE_KEY in promise)) {
+ promise[RESOLVED_VALUE_KEY] = null;
+ promise.then((value) => {
+ return promise[RESOLVED_VALUE_KEY] = value;
+ });
+ }
+
+ // Always return cached value
+ return promise[RESOLVED_VALUE_KEY];
+
+ };
+
+}]);
diff --git a/guacamole/src/main/frontend/src/app/player/directives/player.js b/guacamole/src/main/frontend/src/app/player/directives/player.js
new file mode 100644
index 000000000..78e8adc5a
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/directives/player.js
@@ -0,0 +1,388 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * Directive which plays back session recordings. This directive emits the
+ * following events based on state changes within the current recording:
+ *
+ * "guacPlayerLoading":
+ * A new recording has been selected and is now loading.
+ *
+ * "guacPlayerError":
+ * The current recording cannot be loaded or played due to an error.
+ * The recording may be unreadable (lack of permissions) or corrupt
+ * (protocol error).
+ *
+ * "guacPlayerProgress"
+ * Additional data has been loaded for the current recording and the
+ * recording's duration has changed. The new duration in milliseconds
+ * and the number of bytes loaded so far are passed to the event.
+ *
+ * "guacPlayerLoaded"
+ * The current recording has finished loading.
+ *
+ * "guacPlayerPlay"
+ * Playback of the current recording has started or has been resumed.
+ *
+ * "guacPlayerPause"
+ * Playback of the current recording has been paused.
+ *
+ * "guacPlayerSeek"
+ * The playback position of the current recording has changed. The new
+ * position within the recording is passed to the event as the number
+ * of milliseconds since the start of the recording.
+ */
+angular.module('player').directive('guacPlayer', ['$injector', function guacPlayer($injector) {
+
+ var config = {
+ restrict : 'E',
+ templateUrl : 'app/player/templates/player.html'
+ };
+
+ config.scope = {
+
+ /**
+ * A Blob containing the Guacamole session recording to load.
+ *
+ * @type {!Blob|Guacamole.Tunnel}
+ */
+ src : '='
+
+ };
+
+ config.controller = ['$scope', '$element', '$injector',
+ function guacPlayerController($scope) {
+
+ /**
+ * Guacamole.SessionRecording instance to be used to playback the
+ * session recording given via $scope.src. If the recording has not
+ * yet been loaded, this will be null.
+ *
+ * @type {Guacamole.SessionRecording}
+ */
+ $scope.recording = null;
+
+ /**
+ * The current playback position, in milliseconds. If a seek request is
+ * in progress, this will be the desired playback position of the
+ * pending request.
+ *
+ * @type {!number}
+ */
+ $scope.playbackPosition = 0;
+
+ /**
+ * The key of the translation string that describes the operation
+ * currently running in the background, or null if no such operation is
+ * running.
+ *
+ * @type {string}
+ */
+ $scope.operationMessage = null;
+
+ /**
+ * The current progress toward completion of the operation running in
+ * the background, where 0 represents no progress and 1 represents full
+ * completion. If no such operation is running, this value has no
+ * meaning.
+ *
+ * @type {!number}
+ */
+ $scope.operationProgress = 0;
+
+ /**
+ * The position within the recording of the current seek operation, in
+ * milliseconds. If a seek request is not in progress, this will be
+ * null.
+ *
+ * @type {number}
+ */
+ $scope.seekPosition = null;
+
+ /**
+ * Whether a seek request is currently in progress. A seek request is
+ * in progress if the user is attempting to change the current playback
+ * position (the user is manipulating the playback position slider).
+ *
+ * @type {boolean}
+ */
+ var pendingSeekRequest = false;
+
+ /**
+ * Whether playback should be resumed (play() should be invoked on the
+ * recording) once the current seek request is complete. This value
+ * only has meaning if a seek request is pending.
+ *
+ * @type {boolean}
+ */
+ var resumeAfterSeekRequest = false;
+
+ /**
+ * Formats the given number as a decimal string, adding leading zeroes
+ * such that the string contains at least two digits. The given number
+ * MUST NOT be negative.
+ *
+ * @param {!number} value
+ * The number to format.
+ *
+ * @returns {!string}
+ * The decimal string representation of the given value, padded
+ * with leading zeroes up to a minimum length of two digits.
+ */
+ var zeroPad = function zeroPad(value) {
+ return value > 9 ? value : '0' + value;
+ };
+
+ /**
+ * Formats the given quantity of milliseconds as days, hours, minutes,
+ * and whole seconds, separated by colons (DD:HH:MM:SS). Hours are
+ * included only if the quantity is at least one hour, and days are
+ * included only if the quantity is at least one day. All included
+ * groups are zero-padded to two digits with the exception of the
+ * left-most group.
+ *
+ * @param {!number} value
+ * The time to format, in milliseconds.
+ *
+ * @returns {!string}
+ * The given quantity of milliseconds formatted as "DD:HH:MM:SS".
+ */
+ $scope.formatTime = function formatTime(value) {
+
+ // Round provided value down to whole seconds
+ value = Math.floor((value || 0) / 1000);
+
+ // Separate seconds into logical groups of seconds, minutes,
+ // hours, etc.
+ var groups = [ 1, 24, 60, 60 ];
+ for (var i = groups.length - 1; i >= 0; i--) {
+ var placeValue = groups[i];
+ groups[i] = zeroPad(value % placeValue);
+ value = Math.floor(value / placeValue);
+ }
+
+ // Format groups separated by colons, stripping leading zeroes and
+ // groups which are entirely zeroes, leaving at least minutes and
+ // seconds
+ var formatted = groups.join(':');
+ return /^[0:]*([0-9]{1,2}(?::[0-9]{2})+)$/.exec(formatted)[1];
+
+ };
+
+ /**
+ * Pauses playback and decouples the position slider from current
+ * playback position, allowing the user to manipulate the slider
+ * without interference. Playback state will be resumed following a
+ * call to commitSeekRequest().
+ */
+ $scope.beginSeekRequest = function beginSeekRequest() {
+
+ // If a recording is present, pause and save state if we haven't
+ // already done so
+ if ($scope.recording && !pendingSeekRequest) {
+ resumeAfterSeekRequest = $scope.recording.isPlaying();
+ $scope.recording.pause();
+ }
+
+ // Flag seek request as in progress
+ pendingSeekRequest = true;
+
+ };
+
+ /**
+ * Restores the playback state at the time beginSeekRequest() was
+ * called and resumes coupling between the playback position slider and
+ * actual playback position.
+ */
+ $scope.commitSeekRequest = function commitSeekRequest() {
+
+ // If a recording is present and there is an active seek request,
+ // restore the playback state at the time that request began and
+ // begin seeking to the requested position
+ if ($scope.recording && pendingSeekRequest) {
+
+ $scope.seekPosition = null;
+ $scope.operationMessage = 'PLAYER.INFO_SEEK_IN_PROGRESS';
+ $scope.operationProgress = 0;
+
+ // Cancel seek when requested, updating playback position if
+ // that position changed
+ $scope.cancelOperation = function abortSeek() {
+ $scope.recording.cancel();
+ $scope.playbackPosition = $scope.seekPosition || $scope.playbackPosition;
+ };
+
+ resumeAfterSeekRequest && $scope.recording.play();
+ $scope.recording.seek($scope.playbackPosition, function seekComplete() {
+ $scope.operationMessage = null;
+ $scope.$evalAsync();
+ });
+
+ }
+
+ // Flag seek request as completed
+ pendingSeekRequest = false;
+
+ };
+
+ /**
+ * Toggles the current playback state. If playback is currently paused,
+ * playback is resumed. If playback is currently active, playback is
+ * paused. If no recording has been loaded, this function has no
+ * effect.
+ */
+ $scope.togglePlayback = function togglePlayback() {
+ if ($scope.recording) {
+ if ($scope.recording.isPlaying())
+ $scope.recording.pause();
+ else
+ $scope.recording.play();
+ }
+ };
+
+ // Automatically load the requested session recording
+ $scope.$watch('src', function srcChanged(src) {
+
+ // Reset position and seek state
+ pendingSeekRequest = false;
+ $scope.playbackPosition = 0;
+
+ // Stop loading the current recording, if any
+ if ($scope.recording) {
+ $scope.recording.pause();
+ $scope.recording.abort();
+ }
+
+ // If no recording is provided, reset to empty
+ if (!src)
+ $scope.recording = null;
+
+ // Otherwise, begin loading the provided recording
+ else {
+
+ $scope.recording = new Guacamole.SessionRecording(src);
+
+ // Begin downloading the recording
+ $scope.recording.connect();
+
+ // Notify listeners when the recording is completely loaded
+ $scope.recording.onload = function recordingLoaded() {
+ $scope.operationMessage = null;
+ $scope.$emit('guacPlayerLoaded');
+ $scope.$evalAsync();
+ };
+
+ // Notify listeners if an error occurs
+ $scope.recording.onerror = function recordingFailed(message) {
+ $scope.operationMessage = null;
+ $scope.$emit('guacPlayerError', message);
+ $scope.$evalAsync();
+ };
+
+ // Notify listeners when additional recording data has been
+ // loaded
+ $scope.recording.onprogress = function recordingLoadProgressed(duration, current) {
+ $scope.operationProgress = src.size ? current / src.size : 0;
+ $scope.$emit('guacPlayerProgress', duration, current);
+ $scope.$evalAsync();
+ };
+
+ // Notify listeners when playback has started/resumed
+ $scope.recording.onplay = function playbackStarted() {
+ $scope.$emit('guacPlayerPlay');
+ $scope.$evalAsync();
+ };
+
+ // Notify listeners when playback has paused
+ $scope.recording.onpause = function playbackPaused() {
+ $scope.$emit('guacPlayerPause');
+ $scope.$evalAsync();
+ };
+
+ // Notify listeners when current position within the recording
+ // has changed
+ $scope.recording.onseek = function positionChanged(position, current, total) {
+
+ // Update current playback position while playing
+ if ($scope.recording.isPlaying())
+ $scope.playbackPosition = position;
+
+ // Update seek progress while seeking
+ else {
+ $scope.seekPosition = position;
+ $scope.operationProgress = current / total;
+ }
+
+ $scope.$emit('guacPlayerSeek', position);
+ $scope.$evalAsync();
+
+ };
+
+ $scope.operationMessage = 'PLAYER.INFO_LOADING_RECORDING';
+ $scope.operationProgress = 0;
+
+ $scope.cancelOperation = function abortLoad() {
+ $scope.recording.abort();
+ $scope.operationMessage = null;
+ };
+
+ $scope.$emit('guacPlayerLoading');
+
+ }
+
+ });
+
+ // Clean up resources when player is destroyed
+ $scope.$on('$destroy', function playerDestroyed() {
+ $scope.recording.pause();
+ $scope.recording.abort();
+ });
+
+ }];
+
+ return config;
+
+}]);
diff --git a/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js b/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js
new file mode 100644
index 000000000..20eab7cb8
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * Directive which contains a given Guacamole.Display, automatically scaling
+ * the display to fit available space.
+ */
+angular.module('player').directive('guacPlayerDisplay', [function guacPlayerDisplay() {
+
+ var config = {
+ restrict : 'E',
+ templateUrl : 'app/player/templates/playerDisplay.html'
+ };
+
+ config.scope = {
+
+ /**
+ * The Guacamole.Display instance which should be displayed within the
+ * directive.
+ *
+ * @type {Guacamole.Display}
+ */
+ display : '='
+
+ };
+
+ config.controller = ['$scope', '$element', function guacPlayerDisplayController($scope, $element) {
+
+ /**
+ * The root element of this instance of the guacPlayerDisplay
+ * directive.
+ *
+ * @type {Element}
+ */
+ var element = $element.find('.guac-player-display')[0];
+
+ /**
+ * The element which serves as a container for the root element of the
+ * Guacamole.Display assigned to $scope.display.
+ *
+ * @type {HTMLDivElement}
+ */
+ var container = $element.find('.guac-player-display-container')[0];
+
+ /**
+ * Rescales the Guacamole.Display currently assigned to $scope.display
+ * such that it exactly fits within this directive's available space.
+ * If no display is currently assigned or the assigned display is not
+ * at least 1x1 pixels in size, this function has no effect.
+ */
+ $scope.fitDisplay = function fitDisplay() {
+
+ // Ignore if no display is yet present
+ if (!$scope.display)
+ return;
+
+ var displayWidth = $scope.display.getWidth();
+ var displayHeight = $scope.display.getHeight();
+
+ // Ignore if the provided display is not at least 1x1 pixels
+ if (!displayWidth || !displayHeight)
+ return;
+
+ // Fit display within available space
+ $scope.display.scale(Math.min(element.offsetWidth / displayWidth,
+ element.offsetHeight / displayHeight));
+
+ };
+
+ // Automatically add/remove the Guacamole.Display as $scope.display is
+ // updated
+ $scope.$watch('display', function displayChanged(display, oldDisplay) {
+
+ // Clear out old display, if any
+ if (oldDisplay) {
+ container.innerHTML = '';
+ oldDisplay.onresize = null;
+ }
+
+ // If a new display is provided, add it to the container, keeping
+ // its scale in sync with changes to available space and display
+ // size
+ if (display) {
+ container.appendChild(display.getElement());
+ display.onresize = $scope.fitDisplay;
+ $scope.fitDisplay();
+ }
+
+ });
+
+ }];
+
+ return config;
+
+}]);
diff --git a/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js b/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js
new file mode 100644
index 000000000..1d20fc065
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * Directive which displays an indicator showing the current progress of an
+ * arbitrary operation.
+ */
+angular.module('player').directive('guacPlayerProgressIndicator', [function guacPlayerProgressIndicator() {
+
+ var config = {
+ restrict : 'E',
+ templateUrl : 'app/player/templates/progressIndicator.html'
+ };
+
+ config.scope = {
+
+ /**
+ * A value between 0 and 1 inclusive which indicates current progress,
+ * where 0 represents no progress and 1 represents finished.
+ *
+ * @type {Number}
+ */
+ progress : '='
+
+ };
+
+ config.controller = ['$scope', function guacPlayerProgressIndicatorController($scope) {
+
+ /**
+ * The current progress of the operation as a percentage. This value is
+ * automatically updated as $scope.progress changes.
+ *
+ * @type {Number}
+ */
+ $scope.percentage = 0;
+
+ /**
+ * The CSS transform which should be applied to the bar portion of the
+ * progress indicator. This value is automatically updated as
+ * $scope.progress changes.
+ *
+ * @type {String}
+ */
+ $scope.barTransform = null;
+
+ // Keep percentage and bar transform up-to-date with changes to
+ // progress value
+ $scope.$watch('progress', function progressChanged(progress) {
+ progress = progress || 0;
+ $scope.percentage = Math.floor(progress * 100);
+ $scope.barTransform = 'rotate(' + (360 * progress - 45) + 'deg)';
+ });
+
+ }];
+
+ return config;
+
+}]);
diff --git a/guacamole/src/main/frontend/src/app/player/playerModule.js b/guacamole/src/main/frontend/src/app/player/playerModule.js
new file mode 100644
index 000000000..c3bf3ea4a
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/playerModule.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * Module providing in-browser playback of session recordings.
+ */
+angular.module('player', [
+ 'element'
+]);
diff --git a/guacamole/src/main/frontend/src/app/player/styles/player.css b/guacamole/src/main/frontend/src/app/player/styles/player.css
new file mode 100644
index 000000000..36a783eca
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/styles/player.css
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+guac-player {
+ display: inline-block;
+ position: relative;
+}
+
+guac-player .guac-player-display {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+guac-player .guac-player-controls {
+
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+
+}
+
+.guac-player-controls .guac-player-seek {
+ display: block;
+ width: 100%;
+}
+
+.guac-player-controls .guac-player-play,
+.guac-player-controls .guac-player-pause {
+ color: white;
+ background: transparent;
+ border: none;
+ width: 2em;
+ height: 2em;
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+}
+
+.guac-player-controls .guac-player-play:hover,
+.guac-player-controls .guac-player-pause:hover {
+ background: rgba(255, 255, 255, 0.5);
+}
+
+.guac-player-controls .pause-icon,
+.guac-player-controls .play-icon {
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ vertical-align: middle;
+}
+
+.guac-player-controls .play-icon {
+ background-image: url('images/action-icons/guac-play.svg');
+}
+
+.guac-player-controls .pause-icon {
+ background-image: url('images/action-icons/guac-pause.svg');
+}
+
+guac-player .guac-player-status {
+
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1;
+
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: flex;
+
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+ -moz-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -moz-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -webkit-flex-direction: column;
+ -moz-box-orient: vertical;
+ -moz-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+
+}
diff --git a/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css b/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css
new file mode 100644
index 000000000..c17cd4d65
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+.guac-player-display {
+
+ position: relative;
+
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: flex;
+
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+ -moz-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -moz-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+
+}
+
+.guac-player-display .guac-player-display-container {
+ -webkit-box-flex: 0;
+ -webkit-flex: 0 0 auto;
+ -moz-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+}
diff --git a/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css b/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css
new file mode 100644
index 000000000..7d8c35e5e
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+guac-player-progress-indicator {
+
+ width: 128px;
+ height: 128px;
+
+ position: relative;
+
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: flex;
+
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+ -moz-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -moz-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+
+}
+
+guac-player-progress-indicator .guac-player-progress-text {
+ font-size: 2em;
+ font-weight: bold;
+}
+
+guac-player-progress-indicator .guac-player-progress-bar-container {
+
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 50%;
+ height: 100%;
+ overflow: hidden;
+
+}
+
+guac-player-progress-indicator .guac-player-progress-bar-container.past-halfway {
+ overflow: visible;
+}
+
+guac-player-progress-indicator .guac-player-progress-bar-container.past-halfway::before,
+guac-player-progress-indicator .guac-player-progress-bar {
+
+ position: absolute;
+ left: -64px;
+ top: 0;
+ width: 128px;
+ height: 128px;
+
+ -webkit-border-radius: 128px;
+ -moz-border-radius: 128px;
+ border-radius: 128px;
+
+ border: 12px solid #5AF;
+ border-bottom-color: transparent;
+ border-right-color: transparent;
+
+}
+
+guac-player-progress-indicator .guac-player-progress-bar-container.past-halfway::before {
+
+ content: ' ';
+ display: block;
+ box-sizing: border-box;
+
+ -webkit-transform: rotate(135deg);
+ -moz-transform: rotate(135deg);
+ -ms-transform: rotate(135deg);
+ -o-transform: rotate(135deg);
+ transform: rotate(135deg);
+
+}
diff --git a/guacamole/src/main/frontend/src/app/player/styles/seek.css b/guacamole/src/main/frontend/src/app/player/styles/seek.css
new file mode 100644
index 000000000..4b41f3a0a
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/styles/seek.css
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * NOTE: This session recording player implementation is based on the Session
+ * Recording Player for Glyptodon Enterprise which is available at
+ * https://github.com/glyptodon/glyptodon-enterprise-player under the
+ * following license:
+ *
+ * Copyright (C) 2019 Glyptodon, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/*
+ * General (not browser-specific)
+ */
+
+input[type="range"] {
+ background: transparent;
+ width: 100%;
+ margin: 0;
+}
+
+input[type="range"]:focus {
+ outline: none;
+}
+
+/*
+ * WebKit
+ */
+
+input[type="range"] {
+ -webkit-appearance: none;
+}
+
+input[type="range"]::-webkit-slider-runnable-track {
+
+ border: none;
+ border-radius: 0;
+ background: #5AF;
+
+ width: 100%;
+ height: 0.5em;
+
+ cursor: pointer;
+
+}
+
+input[type="range"]::-webkit-slider-thumb {
+
+ border: none;
+ border-radius: 0;
+ background: white;
+
+ width: 3px;
+ height: 0.5em;
+
+ -webkit-appearance: none;
+ cursor: pointer;
+
+}
+
+input[type="range"]:focus::-webkit-slider-runnable-track {
+ background: #5AF;
+}
+
+/*
+ * Firefox
+ */
+
+input[type="range"]::-moz-range-track {
+
+ border: none;
+ border-radius: 0;
+ background: #5AF;
+
+ width: 100%;
+ height: 0.5em;
+
+ cursor: pointer;
+
+}
+
+input[type="range"]::-moz-range-thumb {
+
+ border: none;
+ border-radius: 0;
+ background: white;
+
+ width: 3px;
+ height: 0.5em;
+
+ cursor: pointer;
+
+}
+
+/*
+ * Internet Explorer
+ */
+
+input[type="range"]::-ms-track {
+
+ width: 100%;
+ height: 0.5em;
+ margin: 0;
+
+ border: none;
+ border-radius: 0;
+ background: transparent;
+ color: transparent;
+
+ cursor: pointer;
+
+}
+
+input[type="range"]::-ms-thumb {
+
+ border: none;
+ border-radius: 0;
+ background: white;
+
+ width: 3px;
+ height: 0.5em;
+ margin: 0;
+
+ cursor: pointer;
+
+}
+
+input[type="range"]::-ms-fill-lower,
+input[type="range"]::-ms-fill-upper,
+input[type="range"]:focus::-ms-fill-lower,
+input[type="range"]:focus::-ms-fill-upper {
+ border: none;
+ border-radius: 0;
+ background: #5AF;
+}
diff --git a/guacamole/src/main/frontend/src/app/player/templates/player.html b/guacamole/src/main/frontend/src/app/player/templates/player.html
new file mode 100644
index 000000000..91594b40e
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/player/templates/player.html
@@ -0,0 +1,41 @@
+
+