From d748068373cc9411f992ed47caadb691fe0270d3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Apr 2017 13:19:46 +1200 Subject: [PATCH 01/94] Stub new avatar recording script that records when tablet is hidden --- scripts/system/record.js | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 scripts/system/record.js diff --git a/scripts/system/record.js b/scripts/system/record.js new file mode 100644 index 0000000000..7e7e687df8 --- /dev/null +++ b/scripts/system/record.js @@ -0,0 +1,87 @@ +"use strict"; + +// +// record.js +// +// Created by David Rowe on 5 Apr 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { + + var APP_NAME = "RECORD", + APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // FIXME: Record icon. + APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", // FIXME: Record icon. + isRecordingEnabled = false, + isRecording = false, + tablet, + button; + + function startRecording() { + isRecording = true; + print("Start recording"); + } + + function finishRecording() { + isRecording = false; + print("Finish recording"); + } + + function abandonRecording() { + isRecording = false; + print("Abandon recording"); + } + + function onTabletShownChanged() { + if (tablet.tabletShown) { + // Finish recording if is recording. + if (isRecording) { + finishRecording(); + button.editProperties({ isActive: isRecordingEnabled || isRecording }); + } + } else { + // Start recording if recording is enabled. + if (isRecordingEnabled) { + startRecording(); + button.editProperties({ isActive: isRecordingEnabled || isRecording }); + } + } + } + + function setUp() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + if (!tablet) { + return; + } + + // Tablet/toolbar button. + button = tablet.addButton({ + icon: APP_ICON_INACTIVE, + activeIcon: APP_ICON_ACTIVE, + text: APP_NAME, + isActive: isRecordingEnabled || isRecording + }); + + // Track showing/hiding tablet. + tablet.tabletShownChanged.connect(onTabletShownChanged); + + } + + function tearDown() { + if (isRecording) { + abandonRecording(); + } + + if (!tablet) { + return; + } + tablet.tabletShownChanged.disconnect(onTabletShownChanged); + tablet.removeButton(button); + } + + setUp(); + Script.scriptEnding.connect(tearDown); +}()); From a625fd45235a81446d4d1e8060d83abf40428055 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Apr 2017 16:20:04 +1200 Subject: [PATCH 02/94] Add dialog with "Enable recording" checkbox --- scripts/system/html/css/record.css | 9 +++++++ scripts/system/html/js/record.js | 42 ++++++++++++++++++++++++++++++ scripts/system/html/record.html | 33 +++++++++++++++++++++++ scripts/system/record.js | 30 ++++++++++++++++++++- 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 scripts/system/html/css/record.css create mode 100644 scripts/system/html/js/record.js create mode 100644 scripts/system/html/record.html diff --git a/scripts/system/html/css/record.css b/scripts/system/html/css/record.css new file mode 100644 index 0000000000..1e91befc27 --- /dev/null +++ b/scripts/system/html/css/record.css @@ -0,0 +1,9 @@ +/* +// record.css +// +// Created by David Rowe on 5 Apr 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +*/ diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js new file mode 100644 index 0000000000..948130c77a --- /dev/null +++ b/scripts/system/html/js/record.js @@ -0,0 +1,42 @@ +"use strict"; + +// +// record.js +// +// Created by David Rowe on 5 Apr 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var enableRecording, + EVENT_BRIDGE_TYPE = "record"; + +function onScriptEventReceived(data) { + var message = JSON.parse(data); + if (message.type === EVENT_BRIDGE_TYPE) { + if (message.action === "enableRecording") { + enableRecording.checked = message.value; + } + } +} + +function onBodyLoaded() { + + EventBridge.scriptEventReceived.connect(onScriptEventReceived); + + enableRecording = document.getElementById("enable-recording"); + enableRecording.onchange = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: "enableRecording", + value: enableRecording.checked + })); + }; + + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: "bodyLoaded", + })); +} diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html new file mode 100644 index 0000000000..ddb465a706 --- /dev/null +++ b/scripts/system/html/record.html @@ -0,0 +1,33 @@ + + + + + Record + + + + + +
+ +
+
+ + +
+
+

Close the tablet to start recording.

+
+ + + diff --git a/scripts/system/record.js b/scripts/system/record.js index 7e7e687df8..5475c92157 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -15,10 +15,12 @@ var APP_NAME = "RECORD", APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // FIXME: Record icon. APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", // FIXME: Record icon. + APP_URL = Script.resolvePath("html/record.html"), isRecordingEnabled = false, isRecording = false, tablet, - button; + button, + EVENT_BRIDGE_TYPE = "record"; function startRecording() { isRecording = true; @@ -51,6 +53,26 @@ } } + function onWebEventReceived(data) { + var message = JSON.parse(data); + if (message.type === EVENT_BRIDGE_TYPE) { + if (message.action === "bodyLoaded") { + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: "enableRecording", + value: isRecordingEnabled + })); + + } else if (message.action === "enableRecording") { + isRecordingEnabled = message.value; + } + } + } + + function onButtonClicked() { + tablet.gotoWebScreen(APP_URL); + } + function setUp() { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { @@ -64,6 +86,10 @@ text: APP_NAME, isActive: isRecordingEnabled || isRecording }); + button.clicked.connect(onButtonClicked); + + // UI communications. + tablet.webEventReceived.connect(onWebEventReceived); // Track showing/hiding tablet. tablet.tabletShownChanged.connect(onTabletShownChanged); @@ -78,7 +104,9 @@ if (!tablet) { return; } + tablet.webEventReceived.disconnect(onWebEventReceived); tablet.tabletShownChanged.disconnect(onTabletShownChanged); + button.clicked.disconnect(onButtonClicked); tablet.removeButton(button); } From 54a0bea8c0c35685ed2534bb16a1df29fb58c837 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Apr 2017 17:48:35 +1200 Subject: [PATCH 03/94] Toolbar icon state and start/stop recording with window closing/opening --- scripts/system/html/js/record.js | 21 ++++++++------ scripts/system/record.js | 48 ++++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 948130c77a..42166939be 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -10,14 +10,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var enableRecording, - EVENT_BRIDGE_TYPE = "record"; +var elEnableRecording, + elInstructions, + EVENT_BRIDGE_TYPE = "record", + BODY_LOADED_ACTION = "bodyLoaded", + ENABLE_RECORDING_ACTION = "enableRecording"; function onScriptEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { - if (message.action === "enableRecording") { - enableRecording.checked = message.value; + if (message.action === ENABLE_RECORDING_ACTION) { + elEnableRecording.checked = message.value; } } } @@ -26,17 +29,17 @@ function onBodyLoaded() { EventBridge.scriptEventReceived.connect(onScriptEventReceived); - enableRecording = document.getElementById("enable-recording"); - enableRecording.onchange = function () { + elEnableRecording = document.getElementById("enable-recording"); + elEnableRecording.onchange = function () { EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, - action: "enableRecording", - value: enableRecording.checked + action: ENABLE_RECORDING_ACTION, + value: elEnableRecording.checked })); }; EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, - action: "bodyLoaded", + action: BODY_LOADED_ACTION })); } diff --git a/scripts/system/record.js b/scripts/system/record.js index 5475c92157..7ef8508ad5 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -20,7 +20,9 @@ isRecording = false, tablet, button, - EVENT_BRIDGE_TYPE = "record"; + EVENT_BRIDGE_TYPE = "record", + BODY_LOADED_ACTION = "bodyLoaded", + ENABLE_RECORDING_ACTION = "enableRecording"; function startRecording() { isRecording = true; @@ -37,34 +39,57 @@ print("Abandon recording"); } - function onTabletShownChanged() { - if (tablet.tabletShown) { + function onTabletScreenChanged(type, url) { + // Open/close dialog in tablet or window. + + var RECORD_URL = "/scripts/system/html/record.html", + HOME_URL = "Tablet.qml"; + + if (type === "Home" && url === HOME_URL) { + // Start recording if recording is enabled. + if (!isRecording && isRecordingEnabled) { + startRecording(); + button.editProperties({ isActive: isRecordingEnabled || isRecording }); + } + } else if (type === "Web" && url.slice(-RECORD_URL.length) === RECORD_URL) { // Finish recording if is recording. if (isRecording) { finishRecording(); button.editProperties({ isActive: isRecordingEnabled || isRecording }); } - } else { + } + } + + function onTabletShownChanged() { + // Open/close tablet. + + if (!tablet.tabletShown) { // Start recording if recording is enabled. - if (isRecordingEnabled) { + if (!isRecording && isRecordingEnabled) { startRecording(); button.editProperties({ isActive: isRecordingEnabled || isRecording }); } + } else { + // Finish recording if is recording. + if (isRecording) { + finishRecording(); + button.editProperties({ isActive: isRecordingEnabled || isRecording }); + } } } function onWebEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { - if (message.action === "bodyLoaded") { + if (message.action === BODY_LOADED_ACTION) { tablet.emitScriptEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, - action: "enableRecording", + action: ENABLE_RECORDING_ACTION, value: isRecordingEnabled })); - - } else if (message.action === "enableRecording") { + } else if (message.action === ENABLE_RECORDING_ACTION) { isRecordingEnabled = message.value; + button.editProperties({ isActive: isRecordingEnabled || isRecording }); } } } @@ -91,9 +116,9 @@ // UI communications. tablet.webEventReceived.connect(onWebEventReceived); - // Track showing/hiding tablet. + // Track showing/hiding tablet/dialog. + tablet.screenChanged.connect(onTabletScreenChanged); tablet.tabletShownChanged.connect(onTabletShownChanged); - } function tearDown() { @@ -106,6 +131,7 @@ } tablet.webEventReceived.disconnect(onWebEventReceived); tablet.tabletShownChanged.disconnect(onTabletShownChanged); + tablet.screenChanged.disconnect(onTabletScreenChanged); button.clicked.disconnect(onButtonClicked); tablet.removeButton(button); } From b29bfd4bb42954a2718fe05afcc35c02ee310fcd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Apr 2017 17:49:13 +1200 Subject: [PATCH 04/94] Dialog instructions refer to tablet or window as appropriate --- scripts/system/html/js/record.js | 9 ++++++++- scripts/system/record.js | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 42166939be..0f9e822893 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -14,13 +14,18 @@ var elEnableRecording, elInstructions, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", - ENABLE_RECORDING_ACTION = "enableRecording"; + USING_TOOLBAR_ACTION = "usingToolbar", + ENABLE_RECORDING_ACTION = "enableRecording", + TABLET_INSTRUCTIONS = "Close the tablet to start recording", + WINDOW_INSTRUCTIONS = "Close the window to start recording"; function onScriptEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { if (message.action === ENABLE_RECORDING_ACTION) { elEnableRecording.checked = message.value; + } else if (message.action === USING_TOOLBAR_ACTION) { + elInstructions.innerHTML = message.value ? WINDOW_INSTRUCTIONS : TABLET_INSTRUCTIONS; } } } @@ -38,6 +43,8 @@ function onBodyLoaded() { })); }; + elInstructions = document.getElementById("instructions"); + EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: BODY_LOADED_ACTION diff --git a/scripts/system/record.js b/scripts/system/record.js index 7ef8508ad5..7d12635d77 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -22,8 +22,14 @@ button, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", + USING_TOOLBAR_ACTION = "usingToolbar", ENABLE_RECORDING_ACTION = "enableRecording"; + function usingToolbar() { + return ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))); + } + function startRecording() { isRecording = true; print("Start recording"); @@ -87,6 +93,11 @@ action: ENABLE_RECORDING_ACTION, value: isRecordingEnabled })); + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: USING_TOOLBAR_ACTION, + value: usingToolbar() + })); } else if (message.action === ENABLE_RECORDING_ACTION) { isRecordingEnabled = message.value; button.editProperties({ isActive: isRecordingEnabled || isRecording }); From 813a5684e3b85809b47caa619d25260e2177f8ad Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 6 Apr 2017 09:22:01 +1200 Subject: [PATCH 05/94] Display instruction text only if recording is enabled --- scripts/system/html/js/record.js | 12 ++++++++++-- scripts/system/html/record.html | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 0f9e822893..547b54aae7 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -10,7 +10,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var elEnableRecording, +var isUsingToolbar = false, + elEnableRecording, elInstructions, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", @@ -19,13 +20,19 @@ var elEnableRecording, TABLET_INSTRUCTIONS = "Close the tablet to start recording", WINDOW_INSTRUCTIONS = "Close the window to start recording"; +function updateInstructions() { + elInstructions.innerHTML = elEnableRecording.checked ? (isUsingToolbar ? WINDOW_INSTRUCTIONS : TABLET_INSTRUCTIONS) : ""; +} + function onScriptEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { if (message.action === ENABLE_RECORDING_ACTION) { elEnableRecording.checked = message.value; + updateInstructions(); } else if (message.action === USING_TOOLBAR_ACTION) { - elInstructions.innerHTML = message.value ? WINDOW_INSTRUCTIONS : TABLET_INSTRUCTIONS; + isUsingToolbar = message.value; + updateInstructions(); } } } @@ -36,6 +43,7 @@ function onBodyLoaded() { elEnableRecording = document.getElementById("enable-recording"); elEnableRecording.onchange = function () { + updateInstructions(); EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: ENABLE_RECORDING_ACTION, diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index ddb465a706..85ceca93a5 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -24,7 +24,7 @@
-

Close the tablet to start recording.

+

diff --git a/scripts/system/record.js b/scripts/system/record.js index 809a43b915..b50fe08750 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -20,14 +20,11 @@ isRecordingEnabled = false, tablet, button, - EVENT_BRIDGE_TYPE = "record", - BODY_LOADED_ACTION = "bodyLoaded", - USING_TOOLBAR_ACTION = "usingToolbar", - ENABLE_RECORDING_ACTION = "enableRecording", CountdownTimer, Recorder, - Player; + Player, + Dialog; function updateButtonState() { button.editProperties({ isActive: isRecordingEnabled || !Recorder.isIdle() }); @@ -296,26 +293,36 @@ PLAYER_COMMAND_PLAY = "play", playerIDs = [], // UUIDs of AC player scripts. - playerIsPlaying = [], // True if AC player script is playing a recording. + playerIsPlayings = [], // True if AC player script is playing a recording. playerRecordings = [], // Assignment client mappings of recordings being played. playerTimestamps = [], // Timestamps of last heartbeat update from player script. updateTimer, UPDATE_INTERVAL = 5000; // Must be > player's HEARTBEAT_INTERVAL. TODO: Final value. + function numberOfPlayers() { + return playerIDs.length; + } + function updatePlayers() { var now = Date.now(), + countBefore = playerIDs.length, i; // Remove players that haven't sent a heartbeat for a while. for (i = playerTimestamps.length - 1; i >= 0; i -= 1) { if (now - playerTimestamps[i] > UPDATE_INTERVAL) { playerIDs.splice(i, 1); - playerIsPlaying.splice(i, 1); + playerIsPlayings.splice(i, 1); playerRecordings.splice(i, 1); playerTimestamps.splice(i, 1); } } + + // Update UI. + if (playerIDs.length !== countBefore) { + Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings); + } } function playRecording(recording, position, orientation) { @@ -330,7 +337,7 @@ orientation = MyAvatar.orientation; } - index = playerIsPlaying.indexOf(false); + index = playerIsPlayings.indexOf(false); if (index === -1) { error("No assignment client player available to play recording " + recording.slice(4) + "!"); // Remove leading "atp:" from recording. @@ -346,7 +353,7 @@ })); Script.setTimeout(function () { - if (!playerIsPlaying[index] || playerRecordings[index] !== recording) { + if (!playerIsPlayings[index] || playerRecordings[index] !== recording) { error("Didn't start playing recording " + recording.slice(4) + "!"); // Remove leading "atp:" from recording. } @@ -365,9 +372,10 @@ index = playerIDs.length; playerIDs[index] = sender; } - playerIsPlaying[index] = message.playing; + playerIsPlayings[index] = message.playing; playerRecordings[index] = message.recording; playerTimestamps[index] = Date.now(); + Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings); } function setUp() { @@ -387,6 +395,82 @@ return { playRecording: playRecording, + numberOfPlayers: numberOfPlayers, + setUp: setUp, + tearDown: tearDown + }; + }()); + + Dialog = (function () { + var EVENT_BRIDGE_TYPE = "record", + BODY_LOADED_ACTION = "bodyLoaded", + USING_TOOLBAR_ACTION = "usingToolbar", + ENABLE_RECORDING_ACTION = "enableRecording", + RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", + NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers"; + + function onWebEventReceived(data) { + var message = JSON.parse(data); + if (message.type === EVENT_BRIDGE_TYPE) { + if (message.action === BODY_LOADED_ACTION) { + // Dialog's ready; initialize its state. + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: ENABLE_RECORDING_ACTION, + value: isRecordingEnabled + })); + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: USING_TOOLBAR_ACTION, + value: isUsingToolbar() + })); + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: NUMBER_OF_PLAYERS_ACTION, + value: Player.numberOfPlayers() + })); + } else if (message.action === ENABLE_RECORDING_ACTION) { + // User update "enable recording" checkbox. + // The recording state must be idle because the dialog is open. + isRecordingEnabled = message.value; + updateButtonState(); + } + } + } + + function updatePlayerDetails(playerIsPlayings, playerRecordings) { + var recordingsBeingPlayed = [], + length, + i; + + for (i = 0, length = playerIsPlayings.length; i < length; i += 1) { + if (playerIsPlayings[i]) { + recordingsBeingPlayed.push(playerRecordings[i]); + } + } + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: RECORDINGS_BEING_PLAYED_ACTION, + value: JSON.stringify(recordingsBeingPlayed) + })); + + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: NUMBER_OF_PLAYERS_ACTION, + value: playerIsPlayings.length + })); + } + + function setUp() { + tablet.webEventReceived.connect(onWebEventReceived); + } + + function tearDown() { + tablet.webEventReceived.disconnect(onWebEventReceived); + } + + return { + updatePlayerDetails: updatePlayerDetails, setUp: setUp, tearDown: tearDown }; @@ -394,7 +478,6 @@ function onTabletScreenChanged(type, url) { // Open/close dialog in tablet or window. - var RECORD_URL = "/scripts/system/html/record.html", HOME_URL = "Tablet.qml"; @@ -433,30 +516,6 @@ } } - function onWebEventReceived(data) { - var message = JSON.parse(data); - if (message.type === EVENT_BRIDGE_TYPE) { - if (message.action === BODY_LOADED_ACTION) { - // Dialog's ready; initialize its state. - tablet.emitScriptEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: ENABLE_RECORDING_ACTION, - value: isRecordingEnabled - })); - tablet.emitScriptEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: USING_TOOLBAR_ACTION, - value: isUsingToolbar() - })); - } else if (message.action === ENABLE_RECORDING_ACTION) { - // User update "enable recording" checkbox. - // The recording state must be idle because the dialog is open. - isRecordingEnabled = message.value; - updateButtonState(); - } - } - } - function onButtonClicked() { if (isDialogDisplayed) { // Can click icon in toolbar mode; gotoHomeScreen() closes dialog. @@ -483,13 +542,11 @@ }); button.clicked.connect(onButtonClicked); - // UI communications. - tablet.webEventReceived.connect(onWebEventReceived); - // Track showing/hiding tablet/dialog. tablet.screenChanged.connect(onTabletScreenChanged); tablet.tabletShownChanged.connect(onTabletShownChanged); + Dialog.setUp(); Player.setUp(); Recorder.setUp(Player.playRecording); } @@ -501,8 +558,8 @@ Recorder.tearDown(); Player.tearDown(); + Dialog.tearDown(); - tablet.webEventReceived.disconnect(onWebEventReceived); tablet.tabletShownChanged.disconnect(onTabletShownChanged); tablet.screenChanged.disconnect(onTabletScreenChanged); button.clicked.disconnect(onButtonClicked); From 9a9bcaf2ae539ad41c56cb2060d0c2934f6f36ff Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 11 Apr 2017 09:37:57 +1200 Subject: [PATCH 34/94] Stop button stops playing recording --- scripts/system/html/js/record.js | 29 ++++++++++++++++++++++++--- scripts/system/record.js | 34 +++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 9b4850cb6e..ffa1fad638 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -22,6 +22,7 @@ var isUsingToolbar = false, ENABLE_RECORDING_ACTION = "enableRecording", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", + STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", TABLET_INSTRUCTIONS = "Close the tablet to start recording", WINDOW_INSTRUCTIONS = "Close the window to start recording"; @@ -29,24 +30,46 @@ function updateInstructions() { elInstructions.innerHTML = elEnableRecording.checked ? (isUsingToolbar ? WINDOW_INSTRUCTIONS : TABLET_INSTRUCTIONS) : ""; } +function stopPlayingRecording(event) { + var playerID = event.target.getElementsByTagName("input")[0].value; + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: STOP_PLAYING_RECORDING_ACTION, + value: playerID + })); +} + +function orderRecording(a, b) { + return a.filename > b.filename ? 1 : -1; +} + function updateRecordings() { var tbody, tr, td, + span, + input, length, i; - recordingsBeingPlayed.sort(); + recordingsBeingPlayed.sort(orderRecording); tbody = document.createElement("tbody"); for (i = 0, length = recordingsBeingPlayed.length; i < length; i += 1) { tr = document.createElement("tr"); td = document.createElement("td"); - td.innerHTML = recordingsBeingPlayed[i].slice(4); + td.innerHTML = recordingsBeingPlayed[i].filename.slice(4); tr.appendChild(td); td = document.createElement("td"); - td.innerHTML = "x"; + span = document.createElement("span"); + span.innerHTML = "x"; + span.addEventListener("click", stopPlayingRecording); + input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("value", recordingsBeingPlayed[i].playerID); + span.appendChild(input); + td.appendChild(span); tr.appendChild(td); tbody.appendChild(tr); } diff --git a/scripts/system/record.js b/scripts/system/record.js index b50fe08750..483b356d56 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -291,6 +291,7 @@ var HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel", HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel", PLAYER_COMMAND_PLAY = "play", + PLAYER_COMMAND_STOP = "stop", playerIDs = [], // UUIDs of AC player scripts. playerIsPlayings = [], // True if AC player script is playing a recording. @@ -321,7 +322,7 @@ // Update UI. if (playerIDs.length !== countBefore) { - Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings); + Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); } } @@ -361,6 +362,13 @@ } + function stopPlayingRecording(playerID) { + Messages.sendMessage(HIFI_PLAYER_CHANNEL, JSON.stringify({ + player: playerID, + command: PLAYER_COMMAND_STOP + })); + } + function onMessageReceived(channel, message, sender) { // Heartbeat from AC script. var index; @@ -375,7 +383,7 @@ playerIsPlayings[index] = message.playing; playerRecordings[index] = message.recording; playerTimestamps[index] = Date.now(); - Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings); + Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); } function setUp() { @@ -395,6 +403,7 @@ return { playRecording: playRecording, + stopPlayingRecording: stopPlayingRecording, numberOfPlayers: numberOfPlayers, setUp: setUp, tearDown: tearDown @@ -407,12 +416,14 @@ USING_TOOLBAR_ACTION = "usingToolbar", ENABLE_RECORDING_ACTION = "enableRecording", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", - NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers"; + NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", + STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording"; function onWebEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { - if (message.action === BODY_LOADED_ACTION) { + switch (message.action) { + case BODY_LOADED_ACTION: // Dialog's ready; initialize its state. tablet.emitScriptEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, @@ -429,23 +440,32 @@ action: NUMBER_OF_PLAYERS_ACTION, value: Player.numberOfPlayers() })); - } else if (message.action === ENABLE_RECORDING_ACTION) { + break; + case ENABLE_RECORDING_ACTION: // User update "enable recording" checkbox. // The recording state must be idle because the dialog is open. isRecordingEnabled = message.value; updateButtonState(); + break; + case STOP_PLAYING_RECORDING_ACTION: + // Stop the specified player. + Player.stopPlayingRecording(message.value); + break; } } } - function updatePlayerDetails(playerIsPlayings, playerRecordings) { + function updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs) { var recordingsBeingPlayed = [], length, i; for (i = 0, length = playerIsPlayings.length; i < length; i += 1) { if (playerIsPlayings[i]) { - recordingsBeingPlayed.push(playerRecordings[i]); + recordingsBeingPlayed.push({ + filename: playerRecordings[i], + playerID: playerIDs[i] + }); } } tablet.emitScriptEvent(JSON.stringify({ From 559fba39ab522b8ed964d16558c3080a7cd0bb43 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 11 Apr 2017 10:17:52 +1200 Subject: [PATCH 35/94] Remove avatar after stopping recording --- scripts/system/playRecordingAC.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/playRecordingAC.js b/scripts/system/playRecordingAC.js index a9fc793017..e2baa68534 100644 --- a/scripts/system/playRecordingAC.js +++ b/scripts/system/playRecordingAC.js @@ -245,6 +245,7 @@ if (Recording.isPlaying()) { Recording.stopPlaying(); + Agent.isAvatar = false; } isPlayingRecording = false; recordingFilename = ""; From 86baf785aa29bc939b96cdd9b53e5022b4d3493d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 11 Apr 2017 15:23:09 +1200 Subject: [PATCH 36/94] Add "Load" button --- scripts/system/html/js/record.js | 24 +++++++++++++++++++++++- scripts/system/html/record.html | 3 +++ scripts/system/record.js | 7 ++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index ffa1fad638..ed2c4f82e3 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -11,11 +11,13 @@ // var isUsingToolbar = false, + numberOfPlayers = 0, recordingsBeingPlayed = [], elEnableRecording, elInstructions, elRecordingsPlaying, elNumberOfPlayers, + elLoadButton, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", USING_TOOLBAR_ACTION = "usingToolbar", @@ -23,6 +25,7 @@ var isUsingToolbar = false, RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", + LOAD_RECORDING_ACTION = "loadRecording", TABLET_INSTRUCTIONS = "Close the tablet to start recording", WINDOW_INSTRUCTIONS = "Close the window to start recording"; @@ -77,6 +80,14 @@ function updateRecordings() { elRecordingsPlaying.replaceChild(tbody, elRecordingsPlaying.getElementsByTagName("tbody")[0]); } +function updateLoadButton() { + if (numberOfPlayers > recordingsBeingPlayed.length) { + elLoadButton.removeAttribute("disabled"); + } else { + elLoadButton.setAttribute("disabled", "disabled"); + } +} + function onScriptEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { @@ -92,9 +103,12 @@ function onScriptEventReceived(data) { case RECORDINGS_BEING_PLAYED_ACTION: recordingsBeingPlayed = JSON.parse(message.value); updateRecordings(); + updateLoadButton(); break; case NUMBER_OF_PLAYERS_ACTION: - elNumberOfPlayers.innerHTML = message.value; + numberOfPlayers = message.value; + elNumberOfPlayers.innerHTML = numberOfPlayers; + updateLoadButton(); break; } } @@ -118,6 +132,14 @@ function onBodyLoaded() { elRecordingsPlaying = document.getElementById("recordings-playing"); elNumberOfPlayers = document.getElementById("number-of-players"); + elLoadButton = document.getElementById("load-button"); + elLoadButton.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: LOAD_RECORDING_ACTION + })); + } + EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: BODY_LOADED_ACTION diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index 5b2794ecbe..a6952321b6 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -41,6 +41,9 @@

Number of players: 0

+
+ +
diff --git a/scripts/system/record.js b/scripts/system/record.js index 483b356d56..1f2ce07eed 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -417,7 +417,8 @@ ENABLE_RECORDING_ACTION = "enableRecording", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", - STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording"; + STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", + LOAD_RECORDING_ACTION = "loadRecording"; function onWebEventReceived(data) { var message = JSON.parse(data); @@ -451,6 +452,10 @@ // Stop the specified player. Player.stopPlayingRecording(message.value); break; + case LOAD_RECORDING_ACTION: + // User wants to select an ATP recording to play. + log("TODO: Open dialog for user to select ATP recording to play"); + break; } } } From 3ae388bded33bb98d5616d9d61e3ce668470b003 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Apr 2017 13:32:34 +1200 Subject: [PATCH 37/94] Rework auto-playback --- scripts/system/playRecordingAC.js | 205 ++++++++++++++++++++---------- 1 file changed, 138 insertions(+), 67 deletions(-) diff --git a/scripts/system/playRecordingAC.js b/scripts/system/playRecordingAC.js index e2baa68534..0c38f9e375 100644 --- a/scripts/system/playRecordingAC.js +++ b/scripts/system/playRecordingAC.js @@ -39,9 +39,14 @@ ENTITY_DESCRIPTION = "Avatar recording to play back", ENTITIY_POSITION = { x: -16382, y: -16382, z: -16382 }, // Near but not right on domain corner. ENTITY_SEARCH_DELTA = { x: 1, y: 1, z: 1 }, // Allow for position imprecision. - isClaiming = false, - CLAIM_CHECKS = 2, - claimCheckCount; + SEARCH_IDLE = 0, + SEARCH_SEARCHING = 1, + SEARCH_CLAIMING = 2, + SEARCH_PAUSING = 3, + searchState = SEARCH_IDLE, + otherPlayersPlaying, + otherPlayersPlayingCounts, + pauseCount; function onUpdateTimestamp() { userData.timestamp = Date.now(); @@ -49,7 +54,31 @@ EntityViewer.queryOctree(); // Keep up to date ready for find(). } - function create(filename, position, orientation, scriptUUID) { + function id() { + return entityID; + } + + function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + + function onMessageReceived(channel, message, sender) { + var index; + + if (sender !== scriptUUID) { + message = JSON.parse(message); + index = otherPlayersPlaying.indexOf(message.entity); + if (index !== -1) { + otherPlayersPlayingCounts[index] += 1; + } else { + otherPlayersPlaying.push(message.entity); + otherPlayersPlayingCounts.push(1); + } + } + } + + function create(filename, position, orientation) { // Create a new persistence entity (even if already have one but that should never occur). var properties; @@ -59,6 +88,8 @@ Script.clearInterval(updateTimestampTimer); // Just in case. } + searchState = SEARCH_IDLE; + userData = { recording: filename, position: position, @@ -85,74 +116,111 @@ return false; } - function find(scriptUUID) { - // Find a recording that isn't being played. - // AC scripts may simultaneously find the same entity to play because octree updates are not instantaneously - // propagated to AC scripts. To address this, when an entity is found an AC script updates the entity data to - // "claim" the entity then waits two find() calls before checking the entity data hasn't changed and returning that - // entity as "found". I.e., the last script to write the entity data wins. - var isFound = false, - entityIDs, + function find() { + // Find a persistence entity that isn't being played. + // AC scripts may simultaneously find the same entity to play because octree updates aren't instantaneously + // propagated. Additionally, messages are not instantaneous. To address these issues the "find" progresses through + // the following search states: + // - SEARCH_IDLE + // No searching is being performed. + // Return null. + // - SEARCH_SEARCHING + // Looking for an entity that isn't being played (as reported in entity properties) and isn't being claimed (as + // reported by heartbeat messages. If one is found transition to SEARCH_CLAIMING and start reporting the entity + // in heartbeat messages. + // Return null. + // - SEARCH_CLAIMING + // An entity has been found and is reported in heartbeat messages but isn't being played yet. After a period of + // time, if no other players report they're playing that entity then transition to SEARCH_IDLE otherwise + // transition to SEARCH_PAUSING. + // If transitioning to SEARCH_IDLE update the entity userData and return the recording details, otherwise + // return null; + // - SEARCH_PAUSING + // Two or more players have tried to play the same entity. Wait for a randomized period of time before + // transitioning to SEARCH_SEARCHING. + // Return null. + // One of these states is processed each find() call. + var entityIDs, index, - properties; + found = false, + properties, + numberOfClaims, + result = null; - // If have putatively claimed an entity, handle that claim. - if (isClaiming) { - claimCheckCount += 1; + switch (searchState) { - // Wait for octree updates to propagate. - if (claimCheckCount <= CLAIM_CHECKS) { - EntityViewer.queryOctree(); // Update octree ready for next find() call. - return null; - } - isClaiming = false; + case SEARCH_IDLE: + log("Start searching"); + otherPlayersPlaying = []; + otherPlayersPlayingCounts = []; + Messages.subscribe(HIFI_RECORDER_CHANNEL); + Messages.messageReceived.connect(onMessageReceived); + searchState = SEARCH_SEARCHING; + break; - // Complete claim as "found" if still valid. - properties = Entities.getEntityProperties(entityID, ["userData"]); - userData = JSON.parse(properties.userData); - if (userData.scriptUUID === scriptUUID) { - log("Complete claim " + entityID); - userData.timestamp = Date.now(); - Entities.editEntity(entityID, { userData: JSON.stringify(userData) }); - EntityViewer.queryOctree(); // Update octree ready for next find() call. - updateTimestampTimer = Script.setInterval(onUpdateTimestamp, TIMESTAMP_UPDATE_INTERVAL); - return { recording: userData.recording, position: userData.position, orientation: userData.orientation }; - } - - // Otherwise resume searching. - log("Release claim " + entityID); - EntityViewer.queryOctree(); // Update octree ready for next find() call. - return null; - } - - // Find an entity to claim. - entityIDs = Entities.findEntities(ENTITIY_POSITION, ENTITY_SEARCH_DELTA.x); - if (entityIDs.length > 0) { - index = -1; - while (!isFound && index < entityIDs.length - 1) { - // Find recording that isn't being played and hasn't been claimed. - index += 1; - properties = Entities.getEntityProperties(entityIDs[index], ["name", "userData"]); - if (properties.name === ENTITY_NAME) { - userData = JSON.parse(properties.userData); - isFound = (Date.now() - userData.timestamp) > ((CLAIM_CHECKS + 1) * AUTOPLAY_SEARCH_INTERVAL); + case SEARCH_SEARCHING: + // Find an entity that isn't being played or claimed. + entityIDs = Entities.findEntities(ENTITIY_POSITION, ENTITY_SEARCH_DELTA.x); + if (entityIDs.length > 0) { + index = -1; + while (!found && index < entityIDs.length - 1) { + index += 1; + if (otherPlayersPlaying.indexOf(entityIDs[index]) === -1) { + properties = Entities.getEntityProperties(entityIDs[index], ["name", "userData"]); + userData = JSON.parse(properties.userData); + found = properties.name === ENTITY_NAME && userData.recording !== undefined; + } } } + + // Claim entity if found. + if (found) { + log("Claim entity " + entityIDs[index]); + entityID = entityIDs[index]; + searchState = SEARCH_CLAIMING; + } + break; + + case SEARCH_CLAIMING: + // How many other players are claiming (or playing) this entity? + index = otherPlayersPlaying.indexOf(entityID); + numberOfClaims = index !== -1 ? otherPlayersPlayingCounts[index] : 0; + + // Have found an entity to play if no other players are also claiming it. + if (numberOfClaims === 0) { + log("Complete claim " + entityID); + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(HIFI_RECORDER_CHANNEL); + searchState = SEARCH_IDLE; + userData.scriptUUID = scriptUUID; + userData.timestamp = Date.now(); + Entities.editEntity(entityID, { userData: JSON.stringify(userData) }); + updateTimestampTimer = Script.setInterval(onUpdateTimestamp, TIMESTAMP_UPDATE_INTERVAL); + result = { recording: userData.recording, position: userData.position, orientation: userData.orientation }; + break; + } + + // Otherwise back off for a bit before resuming search. + log("Release claim " + entityID + " and pause searching"); + entityID = null; + pauseCount = randomInt(0, otherPlayersPlaying.length); + searchState = SEARCH_PAUSING; + break; + + case SEARCH_PAUSING: + // Resume searching if have paused long enough. + pauseCount -= 1; + if (pauseCount < 0) { + log("Resume searching"); + otherPlayersPlaying = []; + otherPlayersPlayingCounts = []; + searchState = SEARCH_SEARCHING; + } + break; } - // Claim entity if found. - if (isFound) { - log("Claim entity " + entityIDs[index]); - isClaiming = true; - claimCheckCount = 0; - entityID = entityIDs[index]; - userData.scriptUUID = scriptUUID; - userData.timestamp = Date.now(); - Entities.editEntity(entityID, { userData: JSON.stringify(userData) }); - } - - EntityViewer.queryOctree(); // Update octree ready for next find() call. - return null; + EntityViewer.queryOctree(); + return result; } function destroy() { @@ -160,6 +228,7 @@ if (entityID !== null) { // Just in case. Entities.deleteEntity(entityID); entityID = null; + searchState = SEARCH_IDLE; } if (updateTimestampTimer !== null) { // Just in case. Script.clearInterval(updateTimestampTimer); @@ -180,6 +249,7 @@ } return { + id: id, create: create, find: find, destroy: destroy, @@ -212,7 +282,7 @@ } function play(recording, position, orientation) { - if (Entity.create(recording, position, orientation, scriptUUID)) { + if (Entity.create(recording, position, orientation)) { log("Play new recording " + recordingFilename); isPlayingRecording = true; recordingFilename = recording; @@ -225,7 +295,7 @@ function autoPlay() { var recording; - recording = Entity.find(scriptUUID); + recording = Entity.find(); if (recording) { isPlayingRecording = true; recordingFilename = recording.recording; @@ -285,7 +355,8 @@ function sendHeartbeat() { Messages.sendMessage(HIFI_RECORDER_CHANNEL, JSON.stringify({ playing: Player.isPlaying(), - recording: Player.recording() + recording: Player.recording(), + entity: Entity.id() })); heartbeatTimer = Script.setTimeout(sendHeartbeat, HEARTBEAT_INTERVAL); } From 905560b96d8cb5ba091884f660870da9818aca7e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Apr 2017 14:48:15 +1200 Subject: [PATCH 38/94] Remove checkbox and record-on-close functionality --- scripts/system/html/js/record.js | 31 +---------------- scripts/system/html/record.html | 7 ---- scripts/system/record.js | 57 ++++---------------------------- 3 files changed, 8 insertions(+), 87 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index ed2c4f82e3..b2f3ce1934 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -13,25 +13,15 @@ var isUsingToolbar = false, numberOfPlayers = 0, recordingsBeingPlayed = [], - elEnableRecording, - elInstructions, elRecordingsPlaying, elNumberOfPlayers, elLoadButton, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", - USING_TOOLBAR_ACTION = "usingToolbar", - ENABLE_RECORDING_ACTION = "enableRecording", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", - LOAD_RECORDING_ACTION = "loadRecording", - TABLET_INSTRUCTIONS = "Close the tablet to start recording", - WINDOW_INSTRUCTIONS = "Close the window to start recording"; - -function updateInstructions() { - elInstructions.innerHTML = elEnableRecording.checked ? (isUsingToolbar ? WINDOW_INSTRUCTIONS : TABLET_INSTRUCTIONS) : ""; -} + LOAD_RECORDING_ACTION = "loadRecording"; function stopPlayingRecording(event) { var playerID = event.target.getElementsByTagName("input")[0].value; @@ -92,14 +82,6 @@ function onScriptEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { switch (message.action) { - case ENABLE_RECORDING_ACTION: - elEnableRecording.checked = message.value; - updateInstructions(); - break; - case USING_TOOLBAR_ACTION: - isUsingToolbar = message.value; - updateInstructions(); - break; case RECORDINGS_BEING_PLAYED_ACTION: recordingsBeingPlayed = JSON.parse(message.value); updateRecordings(); @@ -118,17 +100,6 @@ function onBodyLoaded() { EventBridge.scriptEventReceived.connect(onScriptEventReceived); - elEnableRecording = document.getElementById("enable-recording"); - elEnableRecording.onchange = function () { - updateInstructions(); - EventBridge.emitWebEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: ENABLE_RECORDING_ACTION, - value: elEnableRecording.checked - })); - }; - - elInstructions = document.getElementById("instructions"); elRecordingsPlaying = document.getElementById("recordings-playing"); elNumberOfPlayers = document.getElementById("number-of-players"); diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index a6952321b6..6c3b0602fe 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -19,13 +19,6 @@
-
- - -
-
-

-
diff --git a/scripts/system/record.js b/scripts/system/record.js index 1f2ce07eed..e987bdef0d 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -17,7 +17,6 @@ APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", // FIXME: Record icon. APP_URL = Script.resolvePath("html/record.html"), isDialogDisplayed = false, - isRecordingEnabled = false, tablet, button, @@ -26,10 +25,6 @@ Player, Dialog; - function updateButtonState() { - button.editProperties({ isActive: isRecordingEnabled || !Recorder.isIdle() }); - } - function log(message) { print(APP_NAME + ": " + message); } @@ -40,11 +35,6 @@ } - function isUsingToolbar() { - return ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))); - } - CountdownTimer = (function () { // Displays countdown overlay. @@ -205,7 +195,6 @@ function startRecording() { recordingState = RECORDING; - updateButtonState(); log("Start recording"); startPosition = MyAvatar.position; startOrientation = MyAvatar.orientation; @@ -217,7 +206,6 @@ error; recordingState = IDLE; - updateButtonState(); log("Finish recording"); Recording.stopRecording(); success = Recording.saveRecordingToAsset(saveRecordingToAssetCallback); @@ -229,26 +217,22 @@ function cancelRecording() { Recording.stopRecording(); recordingState = IDLE; - updateButtonState(); log("Cancel recording"); } function finishCountdown() { recordingState = RECORDING; - updateButtonState(); startRecording(); } function cancelCountdown() { recordingState = IDLE; - updateButtonState(); CountdownTimer.cancel(); log("Cancel countdown"); } function startCountdown() { recordingState = COUNTING_DOWN; - updateButtonState(); log("Start countdown"); CountdownTimer.start(finishCountdown); } @@ -413,8 +397,6 @@ Dialog = (function () { var EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", - USING_TOOLBAR_ACTION = "usingToolbar", - ENABLE_RECORDING_ACTION = "enableRecording", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", @@ -426,28 +408,12 @@ switch (message.action) { case BODY_LOADED_ACTION: // Dialog's ready; initialize its state. - tablet.emitScriptEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: ENABLE_RECORDING_ACTION, - value: isRecordingEnabled - })); - tablet.emitScriptEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: USING_TOOLBAR_ACTION, - value: isUsingToolbar() - })); tablet.emitScriptEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: NUMBER_OF_PLAYERS_ACTION, value: Player.numberOfPlayers() })); break; - case ENABLE_RECORDING_ACTION: - // User update "enable recording" checkbox. - // The recording state must be idle because the dialog is open. - isRecordingEnabled = message.value; - updateButtonState(); - break; case STOP_PLAYING_RECORDING_ACTION: // Stop the specified player. Player.stopPlayingRecording(message.value); @@ -503,35 +469,26 @@ function onTabletScreenChanged(type, url) { // Open/close dialog in tablet or window. - var RECORD_URL = "/scripts/system/html/record.html", - HOME_URL = "Tablet.qml"; + var RECORD_URL = "/scripts/system/html/record.html"; - if (type === "Home" && url === HOME_URL) { - // Start countdown if using toolbar and recording is enabled. - if (isUsingToolbar() && isRecordingEnabled && Recorder.isIdle()) { - Recorder.startCountdown(); - } - isDialogDisplayed = false; - } else if (type === "Web" && url.slice(-RECORD_URL.length) === RECORD_URL) { + if (type === "Web" && url.slice(-RECORD_URL.length) === RECORD_URL) { // Cancel countdown or finish recording. if (Recorder.isCountingDown()) { Recorder.cancelCountdown(); } else if (Recorder.isRecording()) { Recorder.finishRecording(); } + isDialogDisplayed = true; + } else { + isDialogDisplayed = false; } + button.editProperties({ isActive: isDialogDisplayed }); } function onTabletShownChanged() { // Open/close tablet. - isDialogDisplayed = false; - if (!tablet.tabletShown) { - // Start countdown if recording is enabled. - if (isRecordingEnabled && Recorder.isIdle()) { - Recorder.startCountdown(); - } - } else { + if (tablet.tabletShown) { // Cancel countdown or finish recording. if (Recorder.isCountingDown()) { Recorder.cancelCountdown(); From f30dc4b560973bee9708e2ba0d422d3cc91fa8c9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Apr 2017 15:04:29 +1200 Subject: [PATCH 39/94] Add a "record" button to initiate recording --- scripts/system/html/js/record.js | 12 +++++++++++- scripts/system/html/record.html | 3 +++ scripts/system/record.js | 11 ++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index b2f3ce1934..4fe8e4aeb6 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -16,12 +16,14 @@ var isUsingToolbar = false, elRecordingsPlaying, elNumberOfPlayers, elLoadButton, + elRecordButton, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", - LOAD_RECORDING_ACTION = "loadRecording"; + LOAD_RECORDING_ACTION = "loadRecording", + START_RECORDING_ACTION = "startRecording"; function stopPlayingRecording(event) { var playerID = event.target.getElementsByTagName("input")[0].value; @@ -111,6 +113,14 @@ function onBodyLoaded() { })); } + elRecordButton = document.getElementById("record-button"); + elRecordButton.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: START_RECORDING_ACTION + })); + } + EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: BODY_LOADED_ACTION diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index 6c3b0602fe..e61f803575 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -37,6 +37,9 @@
+
+ +
diff --git a/scripts/system/record.js b/scripts/system/record.js index e987bdef0d..fbf3eca0b7 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -400,7 +400,8 @@ RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", - LOAD_RECORDING_ACTION = "loadRecording"; + LOAD_RECORDING_ACTION = "loadRecording", + START_RECORDING_ACTION = "startRecording"; function onWebEventReceived(data) { var message = JSON.parse(data); @@ -422,6 +423,14 @@ // User wants to select an ATP recording to play. log("TODO: Open dialog for user to select ATP recording to play"); break; + case START_RECORDING_ACTION: + // Start making a recording. + tablet.gotoHomeScreen(); // Closes window dialog. + HMD.closeTablet(); + if (Recorder.isIdle()) { + Recorder.startCountdown(); + } + break; } } } From 50a06db1b9bb0f386236e5fc68ee2484ccc75f47 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Apr 2017 15:56:59 +1200 Subject: [PATCH 40/94] Display "REC" indicator when recording --- scripts/system/record.js | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/scripts/system/record.js b/scripts/system/record.js index fbf3eca0b7..d171f1268b 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -21,6 +21,7 @@ button, CountdownTimer, + RecordingIndicator, Recorder, Player, Dialog; @@ -151,6 +152,68 @@ }; }()); + RecordingIndicator = (function () { + // Displays "recording" overlay. + + var hmdOverlay, + HMD_FONT_SIZE = 0.08, + desktopOverlay, + DESKTOP_FONT_SIZE = 24; + + function show() { + // Create both overlays in case user switches desktop/HMD mode. + var screenSize = Controller.getViewportDimensions(), + recordingText = "REC", // Unicode circle \u25cf doesn't render in HMD. + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; + + if (HMD.active) { + // 3D overlay attached to avatar. + hmdOverlay = Overlays.addOverlay("text3d", { + text: recordingText, + dimensions: { x: 3 * HMD_FONT_SIZE, y: HMD_FONT_SIZE }, + parentID: AVATAR_SELF_ID, + localPosition: { x: -1.0, y: 1.1, z: 2.0 }, + color: { red: 255, green: 0, blue: 0 }, + alpha: 0.9, + lineHeight: HMD_FONT_SIZE, + backgroundAlpha: 0, + ignoreRayIntersection: true, + isFacingAvatar: true, + drawInFront: true, + visible: true + }); + } else { + // 2D overlay on desktop. + desktopOverlay = Overlays.addOverlay("text", { + text: recordingText, + width: 3 * DESKTOP_FONT_SIZE, + height: DESKTOP_FONT_SIZE, + x: screenSize.x - 4 * DESKTOP_FONT_SIZE, + y: DESKTOP_FONT_SIZE, + font: { size: DESKTOP_FONT_SIZE }, + color: { red: 255, green: 8, blue: 8 }, + alpha: 1.0, + backgroundAlpha: 0, + visible: true + }); + } + } + + function hide() { + if (desktopOverlay) { + Overlays.deleteOverlay(desktopOverlay); + } + if (hmdOverlay) { + Overlays.deleteOverlay(hmdOverlay); + } + } + + return { + show: show, + hide: hide + }; + }()); + Recorder = (function () { // Makes the recording and uploads it to the domain's Asset Server. @@ -199,6 +262,7 @@ startPosition = MyAvatar.position; startOrientation = MyAvatar.orientation; Recording.startRecording(); + RecordingIndicator.show(); } function finishRecording() { @@ -208,6 +272,7 @@ recordingState = IDLE; log("Finish recording"); Recording.stopRecording(); + RecordingIndicator.hide(); success = Recording.saveRecordingToAsset(saveRecordingToAssetCallback); if (!success) { error("Error saving recording to Asset Server!"); @@ -216,6 +281,7 @@ function cancelRecording() { Recording.stopRecording(); + RecordingIndicator.hide(); recordingState = IDLE; log("Cancel recording"); } From 8bbce53f74458b015e2097ae719a7cc4f92c811c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 12:23:10 +1200 Subject: [PATCH 41/94] Reset player details when domain restarts or user teleports --- scripts/system/record.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/system/record.js b/scripts/system/record.js index d171f1268b..5495ba080c 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -19,6 +19,7 @@ isDialogDisplayed = false, tablet, button, + isConnected, CountdownTimer, RecordingIndicator, @@ -436,6 +437,14 @@ Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); } + function reset() { + playerIDs = []; + playerIsPlayings = []; + playerRecordings = []; + playerTimestamps = []; + Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs); + } + function setUp() { // Messaging with AC scripts. Messages.messageReceived.connect(onMessageReceived); @@ -455,6 +464,7 @@ playRecording: playRecording, stopPlayingRecording: stopPlayingRecording, numberOfPlayers: numberOfPlayers, + reset: reset, setUp: setUp, tearDown: tearDown }; @@ -584,6 +594,17 @@ } } + function onUpdate() { + if (isConnected !== Window.location.isConnected) { + // Server restarted or domain changed. + isConnected = !isConnected; + if (!isConnected) { + // Clear dialog. + Player.reset(); + } + } + } + function setUp() { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { @@ -606,6 +627,9 @@ Dialog.setUp(); Player.setUp(); Recorder.setUp(Player.playRecording); + + isConnected = Window.location.isConnected; + Script.update.connect(onUpdate); } function tearDown() { @@ -613,6 +637,8 @@ return; } + Script.update.disconnect(onUpdate); + Recorder.tearDown(); Player.tearDown(); Dialog.tearDown(); From 464bc174911e909f3e08fe8abe6fa7ce898e1423 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 13:40:20 +1200 Subject: [PATCH 42/94] Column heading for "unload" buttons --- scripts/system/html/record.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index e61f803575..96e15fc1e5 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -24,7 +24,7 @@
- + From 24a68ea685fef0c41fab3cff6cab6c6b244f1faf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 13:40:52 +1200 Subject: [PATCH 43/94] Report the numnber of free players rather than total number --- scripts/system/html/js/record.js | 12 +++++++++--- scripts/system/html/record.html | 8 +++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 4fe8e4aeb6..e915ac418c 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -14,7 +14,7 @@ var isUsingToolbar = false, numberOfPlayers = 0, recordingsBeingPlayed = [], elRecordingsPlaying, - elNumberOfPlayers, + elPlayersUnused, elLoadButton, elRecordButton, EVENT_BRIDGE_TYPE = "record", @@ -38,6 +38,10 @@ function orderRecording(a, b) { return a.filename > b.filename ? 1 : -1; } +function updatePlayersUnused() { + elPlayersUnused.innerHTML = numberOfPlayers - recordingsBeingPlayed.length; +} + function updateRecordings() { var tbody, tr, @@ -70,6 +74,8 @@ function updateRecordings() { } elRecordingsPlaying.replaceChild(tbody, elRecordingsPlaying.getElementsByTagName("tbody")[0]); + + updatePlayersUnused(); } function updateLoadButton() { @@ -91,7 +97,7 @@ function onScriptEventReceived(data) { break; case NUMBER_OF_PLAYERS_ACTION: numberOfPlayers = message.value; - elNumberOfPlayers.innerHTML = numberOfPlayers; + updatePlayersUnused(); updateLoadButton(); break; } @@ -103,7 +109,7 @@ function onBodyLoaded() { EventBridge.scriptEventReceived.connect(onScriptEventReceived); elRecordingsPlaying = document.getElementById("recordings-playing"); - elNumberOfPlayers = document.getElementById("number-of-players"); + elPlayersUnused = document.getElementById("players-unused"); elLoadButton = document.getElementById("load-button"); elLoadButton.onclick = function () { diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index 96e15fc1e5..0358d6af8a 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -29,11 +29,13 @@ + + + + +
Recordings Being PlayedUnload
-
-

Number of players: 0

-
From 5ad44b6caf23108f56a5b1bcd2cf3230cceb32ad Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 13:50:26 +1200 Subject: [PATCH 44/94] Display empty rows for unused players --- scripts/system/html/js/record.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index e915ac418c..10ce14d689 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -73,9 +73,16 @@ function updateRecordings() { tbody.appendChild(tr); } - elRecordingsPlaying.replaceChild(tbody, elRecordingsPlaying.getElementsByTagName("tbody")[0]); + // Empty rows representing available players. + for (i = recordingsBeingPlayed.length, length = numberOfPlayers; i < length; i += 1) { + tr = document.createElement("tr"); + td = document.createElement("td"); + td.colSpan = 2; + tr.appendChild(td); + tbody.appendChild(tr); + } - updatePlayersUnused(); + elRecordingsPlaying.replaceChild(tbody, elRecordingsPlaying.getElementsByTagName("tbody")[0]); } function updateLoadButton() { @@ -93,10 +100,12 @@ function onScriptEventReceived(data) { case RECORDINGS_BEING_PLAYED_ACTION: recordingsBeingPlayed = JSON.parse(message.value); updateRecordings(); + updatePlayersUnused(); updateLoadButton(); break; case NUMBER_OF_PLAYERS_ACTION: numberOfPlayers = message.value; + updateRecordings(); updatePlayersUnused(); updateLoadButton(); break; From e21e15c064b3d6dab1652f8ea69588e1c19f7755 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 14:01:37 +1200 Subject: [PATCH 45/94] Update warning dialog to refer to player "instance" --- scripts/system/record.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/record.js b/scripts/system/record.js index 5495ba080c..2f3dbb0676 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -391,7 +391,7 @@ index = playerIsPlayings.indexOf(false); if (index === -1) { - error("No assignment client player available to play recording " + error("No player instance available to play recording " + recording.slice(4) + "!"); // Remove leading "atp:" from recording. return; } From afd82f09c0e6d279585fb6920b561bd746d12fd2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 15:18:04 +1200 Subject: [PATCH 46/94] Provide instructions on running the AC player script --- scripts/system/html/css/record.css | 4 +++ scripts/system/html/js/record.js | 52 ++++++++++++++++++++++++++++-- scripts/system/html/record.html | 23 +++++++++++-- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/scripts/system/html/css/record.css b/scripts/system/html/css/record.css index 1e91befc27..13325a5f9c 100644 --- a/scripts/system/html/css/record.css +++ b/scripts/system/html/css/record.css @@ -7,3 +7,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ + +.hidden { + display: none; +} diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 10ce14d689..3c0c8598b6 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -11,10 +11,15 @@ // var isUsingToolbar = false, + isDisplayingInstructions = false, numberOfPlayers = 0, recordingsBeingPlayed = [], elRecordingsPlaying, + elRecordings, + elInstructions, elPlayersUnused, + elHideInfoButton, + elShowInfoButton, elLoadButton, elRecordButton, EVENT_BRIDGE_TYPE = "record", @@ -54,6 +59,7 @@ function updateRecordings() { recordingsBeingPlayed.sort(orderRecording); tbody = document.createElement("tbody"); + tbody.id = "recordings"; for (i = 0, length = recordingsBeingPlayed.length; i < length; i += 1) { tr = document.createElement("tr"); @@ -82,7 +88,38 @@ function updateRecordings() { tbody.appendChild(tr); } - elRecordingsPlaying.replaceChild(tbody, elRecordingsPlaying.getElementsByTagName("tbody")[0]); + elRecordingsPlaying.replaceChild(tbody, elRecordings); + elRecordings = document.getElementById("recordings"); +} + +function updateInstructions() { + // Display show/hide instructions buttons if players are available. + if (numberOfPlayers === 0) { + elHideInfoButton.classList.add("hidden"); + elShowInfoButton.classList.add("hidden"); + } else { + elHideInfoButton.classList.remove("hidden"); + elShowInfoButton.classList.remove("hidden"); + } + + // Display instructions if user requested or no players available. + if (isDisplayingInstructions || numberOfPlayers === 0) { + elRecordings.classList.add("hidden"); + elInstructions.classList.remove("hidden"); + } else { + elInstructions.classList.add("hidden"); + elRecordings.classList.remove("hidden"); + } +} + +function showInstructions() { + isDisplayingInstructions = true; + updateInstructions(); +} + +function hideInstructions() { + isDisplayingInstructions = false; + updateInstructions(); } function updateLoadButton() { @@ -101,12 +138,14 @@ function onScriptEventReceived(data) { recordingsBeingPlayed = JSON.parse(message.value); updateRecordings(); updatePlayersUnused(); + updateInstructions(); updateLoadButton(); break; case NUMBER_OF_PLAYERS_ACTION: numberOfPlayers = message.value; updateRecordings(); updatePlayersUnused(); + updateInstructions(); updateLoadButton(); break; } @@ -118,15 +157,22 @@ function onBodyLoaded() { EventBridge.scriptEventReceived.connect(onScriptEventReceived); elRecordingsPlaying = document.getElementById("recordings-playing"); + elRecordings = document.getElementById("recordings"); + elInstructions = document.getElementById("instructions"); elPlayersUnused = document.getElementById("players-unused"); + elHideInfoButton = document.getElementById("hide-info-button"); + elHideInfoButton.onclick = hideInstructions; + elShowInfoButton = document.getElementById("show-info-button"); + elShowInfoButton.onclick = showInstructions; + elLoadButton = document.getElementById("load-button"); elLoadButton.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: LOAD_RECORDING_ACTION })); - } + }; elRecordButton = document.getElementById("record-button"); elRecordButton.onclick = function () { @@ -134,7 +180,7 @@ function onBodyLoaded() { type: EVENT_BRIDGE_TYPE, action: START_RECORDING_ACTION })); - } + }; EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index 0358d6af8a..8a09564d0f 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -27,11 +27,30 @@ Unload - + + + + + +

+ If you're in your own sandbox or are a domain administrator:
+ Right-click the High Fidelity Sandbox icon in your system tray
and click "Settings".
+ In the "Scripts" section add a new Script URL,
playRecordingAC.js, and set # instances to 1 or more.
+ Click "Save and restart".
+ Now you can play recordings in the domain! +

+

+ +

+ + - Number of available instances: + + Number of available instances: + + From c369b1314a2c3c5ee5f44b75ad2d3c62f1a800a7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 16:32:03 +1200 Subject: [PATCH 47/94] Remove countdown overlays --- scripts/system/record.js | 92 ++-------------------------------------- 1 file changed, 3 insertions(+), 89 deletions(-) diff --git a/scripts/system/record.js b/scripts/system/record.js index 2f3dbb0676..5d8c824366 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -38,113 +38,28 @@ } CountdownTimer = (function () { - // Displays countdown overlay. - + // Counts down a few seconds. var countdownTimer, countdownSeconds, COUNTDOWN_SECONDS = 3, - finishCallback, - isHMD = false, - desktopOverlay, - desktopOverlayAdjust, - DESKTOP_FONT_SIZE = 200, - hmdOverlay, - HMD_FONT_SIZE = 0.25; - - function displayOverlay() { - // Create both overlays in case user switches desktop/HMD mode during countdown. - var screenSize = Controller.getViewportDimensions(), - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; - - isHMD = HMD.active; - - desktopOverlayAdjust = { // Adjust overlay position to cater to font rendering. - x: -DESKTOP_FONT_SIZE / 4, - y: -DESKTOP_FONT_SIZE / 2 - 50 - - }; - - desktopOverlay = Overlays.addOverlay("text", { - text: countdownSeconds, - width: DESKTOP_FONT_SIZE, - height: DESKTOP_FONT_SIZE + 20, - x: screenSize.x / 2 + desktopOverlayAdjust.x, - y: screenSize.y / 2 + desktopOverlayAdjust.y, - font: { size: DESKTOP_FONT_SIZE }, - color: { red: 255, green: 255, blue: 255 }, - alpha: 0.9, - backgroundAlpha: 0, - visible: !isHMD - }); - - hmdOverlay = Overlays.addOverlay("text3d", { - text: countdownSeconds, - dimensions: { x: HMD_FONT_SIZE, y: HMD_FONT_SIZE }, - parentID: AVATAR_SELF_ID, - localPosition: { x: 0, y: 0.6, z: 2.0 }, - color: { red: 255, green: 255, blue: 255 }, - alpha: 0.9, - lineHeight: HMD_FONT_SIZE, - backgroundAlpha: 0, - ignoreRayIntersection: true, - isFacingAvatar: true, - drawInFront: true, - visible: isHMD - }); - } - - function updateOverlay() { - var screenSize; - - if (isHMD !== HMD.active) { - if (isHMD) { - Overlays.editOverlay(hmdOverlay, { visible: false }); - } else { - Overlays.editOverlay(desktopOverlay, { visible: false }); - } - isHMD = HMD.active; - } - - if (isHMD) { - Overlays.editOverlay(hmdOverlay, { - text: countdownSeconds, - visible: true - }); - } else { - screenSize = Controller.getViewportDimensions(); - Overlays.editOverlay(desktopOverlay, { - x: screenSize.x / 2 + desktopOverlayAdjust.x, - y: screenSize.y / 2 + desktopOverlayAdjust.y, - text: countdownSeconds, - visible: true - }); - } - } - - function deleteOverlay() { - Overlays.deleteOverlay(desktopOverlay); - Overlays.deleteOverlay(hmdOverlay); - } + finishCallback; function start(onFinishCallback) { finishCallback = onFinishCallback; countdownSeconds = COUNTDOWN_SECONDS; - displayOverlay(); countdownTimer = Script.setInterval(function () { countdownSeconds -= 1; if (countdownSeconds <= 0) { Script.clearInterval(countdownTimer); - deleteOverlay(); finishCallback(); } else { - updateOverlay(); + // TODO: Tick. } }, 1000); } function cancel() { Script.clearInterval(countdownTimer); - deleteOverlay(); } return { @@ -217,7 +132,6 @@ Recorder = (function () { // Makes the recording and uploads it to the domain's Asset Server. - var IDLE = 0, COUNTING_DOWN = 1, RECORDING = 2, From 84cfe1cb75345e5cae2090dec54dc1878fee1661 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 17:00:35 +1200 Subject: [PATCH 48/94] Don't close dialog when start recording; press "Record" again to finish --- scripts/system/html/css/record.css | 4 +++ scripts/system/html/js/record.js | 55 +++++++++++++++++++++--------- scripts/system/record.js | 13 +++++-- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/scripts/system/html/css/record.css b/scripts/system/html/css/record.css index 13325a5f9c..f9cb76780c 100644 --- a/scripts/system/html/css/record.css +++ b/scripts/system/html/css/record.css @@ -11,3 +11,7 @@ .hidden { display: none; } + +input[type=button].red.pressed { + background: linear-gradient(#94132e, #94132e); +} diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 3c0c8598b6..393d4c246f 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -12,6 +12,7 @@ var isUsingToolbar = false, isDisplayingInstructions = false, + isRecording = false, numberOfPlayers = 0, recordingsBeingPlayed = [], elRecordingsPlaying, @@ -28,7 +29,8 @@ var isUsingToolbar = false, NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", LOAD_RECORDING_ACTION = "loadRecording", - START_RECORDING_ACTION = "startRecording"; + START_RECORDING_ACTION = "startRecording", + STOP_RECORDING_ACTION = "stopRecording"; function stopPlayingRecording(event) { var playerID = event.target.getElementsByTagName("input")[0].value; @@ -152,6 +154,38 @@ function onScriptEventReceived(data) { } } +function onLoadButtonClicked() { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: LOAD_RECORDING_ACTION + })); +} + +function onRecordButtonClicked() { + if (!isRecording) { + elRecordButton.classList.add("pressed"); + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: START_RECORDING_ACTION + })); + isRecording = true; + } else { + elRecordButton.classList.remove("pressed"); + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: STOP_RECORDING_ACTION + })); + isRecording = false; + } +} + +function signalBodyLoaded() { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: BODY_LOADED_ACTION + })); +} + function onBodyLoaded() { EventBridge.scriptEventReceived.connect(onScriptEventReceived); @@ -167,23 +201,10 @@ function onBodyLoaded() { elShowInfoButton.onclick = showInstructions; elLoadButton = document.getElementById("load-button"); - elLoadButton.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: LOAD_RECORDING_ACTION - })); - }; + elLoadButton.onclick = onLoadButtonClicked; elRecordButton = document.getElementById("record-button"); - elRecordButton.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: START_RECORDING_ACTION - })); - }; + elRecordButton.onclick = onRecordButtonClicked; - EventBridge.emitWebEvent(JSON.stringify({ - type: EVENT_BRIDGE_TYPE, - action: BODY_LOADED_ACTION - })); + signalBodyLoaded(); } diff --git a/scripts/system/record.js b/scripts/system/record.js index 5d8c824366..0290f1e083 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -391,7 +391,8 @@ NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", LOAD_RECORDING_ACTION = "loadRecording", - START_RECORDING_ACTION = "startRecording"; + START_RECORDING_ACTION = "startRecording", + STOP_RECORDING_ACTION = "stopRecording"; function onWebEventReceived(data) { var message = JSON.parse(data); @@ -415,12 +416,18 @@ break; case START_RECORDING_ACTION: // Start making a recording. - tablet.gotoHomeScreen(); // Closes window dialog. - HMD.closeTablet(); if (Recorder.isIdle()) { Recorder.startCountdown(); } break; + case STOP_RECORDING_ACTION: + // Cancel or finish a recording. + if (Recorder.isCountingDown()) { + Recorder.cancelCountdown(); + } else if (Recorder.isRecording()) { + Recorder.finishRecording(); + } + break; } } } From b508d6882f3339b696bc67ceafb9b426202a0ce8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 17:59:28 +1200 Subject: [PATCH 49/94] Add checkbox that stops countdown/finishes recording when reopen dialog --- scripts/system/html/js/record.js | 41 ++++++++++++++++++++++---- scripts/system/html/record.html | 4 +++ scripts/system/record.js | 50 +++++++++++++++++++++++++------- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 393d4c246f..7ed44e6d9c 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -23,14 +23,18 @@ var isUsingToolbar = false, elShowInfoButton, elLoadButton, elRecordButton, + elFinishOnOpen, + elFinishOnOpenLabel, EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", + USING_TOOLBAR_ACTION = "usingToolbar", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", LOAD_RECORDING_ACTION = "loadRecording", START_RECORDING_ACTION = "startRecording", - STOP_RECORDING_ACTION = "stopRecording"; + STOP_RECORDING_ACTION = "stopRecording", + FINISH_ON_OPEN_ACTION = "finishOnOpen"; function stopPlayingRecording(event) { var playerID = event.target.getElementsByTagName("input")[0].value; @@ -41,14 +45,14 @@ function stopPlayingRecording(event) { })); } -function orderRecording(a, b) { - return a.filename > b.filename ? 1 : -1; -} - function updatePlayersUnused() { elPlayersUnused.innerHTML = numberOfPlayers - recordingsBeingPlayed.length; } +function orderRecording(a, b) { + return a.filename > b.filename ? 1 : -1; +} + function updateRecordings() { var tbody, tr, @@ -132,10 +136,24 @@ function updateLoadButton() { } } +function updateFinishOnOpenLabel() { + var WINDOW_FINISH_ON_OPEN_LABEL = "Finish recording when open dialog", + TABLET_FINISH_ON_OPEN_LABEL = "Finish recording when open tablet"; + + elFinishOnOpenLabel.innerHTML = isUsingToolbar ? WINDOW_FINISH_ON_OPEN_LABEL : TABLET_FINISH_ON_OPEN_LABEL; +} + function onScriptEventReceived(data) { var message = JSON.parse(data); if (message.type === EVENT_BRIDGE_TYPE) { switch (message.action) { + case USING_TOOLBAR_ACTION: + isUsingToolbar = message.value; + updateFinishOnOpenLabel(); + break; + case FINISH_ON_OPEN_ACTION: + elFinishOnOpen.checked = message.value; + break; case RECORDINGS_BEING_PLAYED_ACTION: recordingsBeingPlayed = JSON.parse(message.value); updateRecordings(); @@ -179,6 +197,14 @@ function onRecordButtonClicked() { } } +function onFinishOnOpenClicked() { + EventBridge.emitWebEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: FINISH_ON_OPEN_ACTION, + value: elFinishOnOpen.checked + })); +} + function signalBodyLoaded() { EventBridge.emitWebEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, @@ -206,5 +232,10 @@ function onBodyLoaded() { elRecordButton = document.getElementById("record-button"); elRecordButton.onclick = onRecordButtonClicked; + elFinishOnOpen = document.getElementById("finish-on-open"); + elFinishOnOpen.onclick = onFinishOnOpenClicked; + + elFinishOnOpenLabel = document.getElementById("finish-on-open-label"); + signalBodyLoaded(); } diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index 8a09564d0f..5eb18de491 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -61,6 +61,10 @@
+
+ + +
diff --git a/scripts/system/record.js b/scripts/system/record.js index 0290f1e083..a3ceda32b3 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -385,14 +385,22 @@ }()); Dialog = (function () { - var EVENT_BRIDGE_TYPE = "record", + var isFinishOnOpen = false, + EVENT_BRIDGE_TYPE = "record", BODY_LOADED_ACTION = "bodyLoaded", + USING_TOOLBAR_ACTION = "usingToolbar", RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed", NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers", STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording", LOAD_RECORDING_ACTION = "loadRecording", START_RECORDING_ACTION = "startRecording", - STOP_RECORDING_ACTION = "stopRecording"; + STOP_RECORDING_ACTION = "stopRecording", + FINISH_ON_OPEN_ACTION = "finishOnOpen"; + + function isUsingToolbar() { + return ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))); + } function onWebEventReceived(data) { var message = JSON.parse(data); @@ -400,6 +408,16 @@ switch (message.action) { case BODY_LOADED_ACTION: // Dialog's ready; initialize its state. + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: USING_TOOLBAR_ACTION, + value: isUsingToolbar() + })); + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: FINISH_ON_OPEN_ACTION, + value: isFinishOnOpen + })); tablet.emitScriptEvent(JSON.stringify({ type: EVENT_BRIDGE_TYPE, action: NUMBER_OF_PLAYERS_ACTION, @@ -428,6 +446,10 @@ Recorder.finishRecording(); } break; + case FINISH_ON_OPEN_ACTION: + // Set behavior on dialog open. + isFinishOnOpen = message.value; + break; } } } @@ -458,6 +480,10 @@ })); } + function finishOnOpen() { + return isFinishOnOpen; + } + function setUp() { tablet.webEventReceived.connect(onWebEventReceived); } @@ -468,21 +494,24 @@ return { updatePlayerDetails: updatePlayerDetails, + finishOnOpen: finishOnOpen, setUp: setUp, tearDown: tearDown }; }()); function onTabletScreenChanged(type, url) { - // Open/close dialog in tablet or window. + // Opened/closed dialog in tablet or window. var RECORD_URL = "/scripts/system/html/record.html"; if (type === "Web" && url.slice(-RECORD_URL.length) === RECORD_URL) { - // Cancel countdown or finish recording. - if (Recorder.isCountingDown()) { - Recorder.cancelCountdown(); - } else if (Recorder.isRecording()) { - Recorder.finishRecording(); + if (Dialog.finishOnOpen()) { + // Cancel countdown or finish recording. + if (Recorder.isCountingDown()) { + Recorder.cancelCountdown(); + } else if (Recorder.isRecording()) { + Recorder.finishRecording(); + } } isDialogDisplayed = true; } else { @@ -492,9 +521,8 @@ } function onTabletShownChanged() { - // Open/close tablet. - - if (tablet.tabletShown) { + // Opened/closed tablet. + if (tablet.tabletShown && Dialog.finishOnOpen()) { // Cancel countdown or finish recording. if (Recorder.isCountingDown()) { Recorder.cancelCountdown(); From 45ca8438fa4c226854a67b4cb9f16b2f49fc89d0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 18:29:39 +1200 Subject: [PATCH 50/94] Persist finish-on-open checkbox value to settings --- scripts/system/record.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/system/record.js b/scripts/system/record.js index a3ceda32b3..f257ecac18 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -395,7 +395,8 @@ LOAD_RECORDING_ACTION = "loadRecording", START_RECORDING_ACTION = "startRecording", STOP_RECORDING_ACTION = "stopRecording", - FINISH_ON_OPEN_ACTION = "finishOnOpen"; + FINISH_ON_OPEN_ACTION = "finishOnOpen", + SETTINGS_FINISH_ON_OPEN = "record/finishOnOpen"; function isUsingToolbar() { return ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) @@ -449,6 +450,7 @@ case FINISH_ON_OPEN_ACTION: // Set behavior on dialog open. isFinishOnOpen = message.value; + Settings.setValue(SETTINGS_FINISH_ON_OPEN, isFinishOnOpen); break; } } @@ -485,6 +487,7 @@ } function setUp() { + isFinishOnOpen = Settings.getValue(SETTINGS_FINISH_ON_OPEN) === true; tablet.webEventReceived.connect(onWebEventReceived); } From ff42715a152085fff5b3ed6439640bc4f4e59a8a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 18:41:55 +1200 Subject: [PATCH 51/94] Retain "Record" button's pressed state when close and reopen dialog --- scripts/system/html/js/record.js | 6 ++++++ scripts/system/record.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 7ed44e6d9c..2adeca8768 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -154,6 +154,12 @@ function onScriptEventReceived(data) { case FINISH_ON_OPEN_ACTION: elFinishOnOpen.checked = message.value; break; + case START_RECORDING_ACTION: + isRecording = message.value; + if (isRecording) { + elRecordButton.classList.add("pressed"); + } + break; case RECORDINGS_BEING_PLAYED_ACTION: recordingsBeingPlayed = JSON.parse(message.value); updateRecordings(); diff --git a/scripts/system/record.js b/scripts/system/record.js index f257ecac18..80ef7651b4 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -424,6 +424,11 @@ action: NUMBER_OF_PLAYERS_ACTION, value: Player.numberOfPlayers() })); + tablet.emitScriptEvent(JSON.stringify({ + type: EVENT_BRIDGE_TYPE, + action: START_RECORDING_ACTION, + value: !Recorder.isIdle() + })); break; case STOP_PLAYING_RECORDING_ACTION: // Stop the specified player. From 9638b093d8424680dde6ea9117ce3e84ce1bca98 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 18:55:02 +1200 Subject: [PATCH 52/94] Fix number of available player instances being wrong on tablet --- scripts/system/playRecordingAC.js | 4 ++++ scripts/system/record.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/scripts/system/playRecordingAC.js b/scripts/system/playRecordingAC.js index 0c38f9e375..5db19c6b28 100644 --- a/scripts/system/playRecordingAC.js +++ b/scripts/system/playRecordingAC.js @@ -369,6 +369,10 @@ } function onMessageReceived(channel, message, sender) { + if (channel !== HIFI_RECORDER_CHANNEL) { + return; + } + message = JSON.parse(message); if (message.player === scriptUUID) { switch (message.command) { diff --git a/scripts/system/record.js b/scripts/system/record.js index 80ef7651b4..3be41e59ac 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -338,6 +338,10 @@ // Heartbeat from AC script. var index; + if (channel !== HIFI_RECORDER_CHANNEL) { + return; + } + message = JSON.parse(message); index = playerIDs.indexOf(sender); From 8f3789421f00787b1857a076b98014ec3dc86ad3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 19:23:22 +1200 Subject: [PATCH 53/94] Show "busy" spinner when counting down or recording --- scripts/system/html/js/record.js | 33 +++++++++++--- scripts/system/html/record.html | 78 +++++++++++++++++--------------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 2adeca8768..c45dde1f53 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -15,13 +15,15 @@ var isUsingToolbar = false, isRecording = false, numberOfPlayers = 0, recordingsBeingPlayed = [], - elRecordingsPlaying, elRecordings, + elRecordingsPlaying, + elRecordingsList, elInstructions, elPlayersUnused, elHideInfoButton, elShowInfoButton, elLoadButton, + elSpinner, elRecordButton, elFinishOnOpen, elFinishOnOpenLabel, @@ -65,7 +67,7 @@ function updateRecordings() { recordingsBeingPlayed.sort(orderRecording); tbody = document.createElement("tbody"); - tbody.id = "recordings"; + tbody.id = "recordings-list"; for (i = 0, length = recordingsBeingPlayed.length; i < length; i += 1) { tr = document.createElement("tr"); @@ -94,8 +96,8 @@ function updateRecordings() { tbody.appendChild(tr); } - elRecordingsPlaying.replaceChild(tbody, elRecordings); - elRecordings = document.getElementById("recordings"); + elRecordingsPlaying.replaceChild(tbody, elRecordingsList); + elRecordingsList = document.getElementById("recordings-list"); } function updateInstructions() { @@ -110,11 +112,11 @@ function updateInstructions() { // Display instructions if user requested or no players available. if (isDisplayingInstructions || numberOfPlayers === 0) { - elRecordings.classList.add("hidden"); + elRecordingsList.classList.add("hidden"); elInstructions.classList.remove("hidden"); } else { elInstructions.classList.add("hidden"); - elRecordings.classList.remove("hidden"); + elRecordingsList.classList.remove("hidden"); } } @@ -136,6 +138,16 @@ function updateLoadButton() { } } +function updateSpinner() { + if (isRecording) { + elRecordings.classList.add("hidden"); + elSpinner.classList.remove("hidden"); + } else { + elSpinner.classList.add("hidden"); + elRecordings.classList.remove("hidden"); + } +} + function updateFinishOnOpenLabel() { var WINDOW_FINISH_ON_OPEN_LABEL = "Finish recording when open dialog", TABLET_FINISH_ON_OPEN_LABEL = "Finish recording when open tablet"; @@ -159,6 +171,7 @@ function onScriptEventReceived(data) { if (isRecording) { elRecordButton.classList.add("pressed"); } + updateSpinner(); break; case RECORDINGS_BEING_PLAYED_ACTION: recordingsBeingPlayed = JSON.parse(message.value); @@ -193,6 +206,7 @@ function onRecordButtonClicked() { action: START_RECORDING_ACTION })); isRecording = true; + updateSpinner(); } else { elRecordButton.classList.remove("pressed"); EventBridge.emitWebEvent(JSON.stringify({ @@ -200,6 +214,7 @@ function onRecordButtonClicked() { action: STOP_RECORDING_ACTION })); isRecording = false; + updateSpinner(); } } @@ -222,8 +237,10 @@ function onBodyLoaded() { EventBridge.scriptEventReceived.connect(onScriptEventReceived); - elRecordingsPlaying = document.getElementById("recordings-playing"); elRecordings = document.getElementById("recordings"); + + elRecordingsPlaying = document.getElementById("recordings-playing"); + elRecordingsList = document.getElementById("recordings-list"); elInstructions = document.getElementById("instructions"); elPlayersUnused = document.getElementById("players-unused"); @@ -235,6 +252,8 @@ function onBodyLoaded() { elLoadButton = document.getElementById("load-button"); elLoadButton.onclick = onLoadButtonClicked; + elSpinner = document.getElementById("spinner"); + elRecordButton = document.getElementById("record-button"); elRecordButton.onclick = onRecordButtonClicked; diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index 5eb18de491..cf70fb2401 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -19,44 +19,48 @@
-
- - - - - - - - - - - - - - - - - - - -
Recordings Being PlayedUnload
+
+
+ + + + + + + + + + + + + + + + + + +
Recordings Being PlayedUnload
+
+
+ +
-
- +
From 75e4fb3820e22772d43f6a3a55b0c0cb587cbf9d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Apr 2017 19:28:40 +1200 Subject: [PATCH 54/94] On tablet with option enabled recording ends when open tablet or dialog --- scripts/system/html/js/record.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index c45dde1f53..20f3e0fb2b 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -149,8 +149,8 @@ function updateSpinner() { } function updateFinishOnOpenLabel() { - var WINDOW_FINISH_ON_OPEN_LABEL = "Finish recording when open dialog", - TABLET_FINISH_ON_OPEN_LABEL = "Finish recording when open tablet"; + var WINDOW_FINISH_ON_OPEN_LABEL = "Finish recording when reopen dialog", + TABLET_FINISH_ON_OPEN_LABEL = "Finish recording when reopen dialog or tablet"; elFinishOnOpenLabel.innerHTML = isUsingToolbar ? WINDOW_FINISH_ON_OPEN_LABEL : TABLET_FINISH_ON_OPEN_LABEL; } From 072e06123f5c18634974b01371bb554bfd437a48 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Apr 2017 10:02:15 +1200 Subject: [PATCH 55/94] Fix recording just made not being played --- scripts/system/playRecordingAC.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/playRecordingAC.js b/scripts/system/playRecordingAC.js index 5db19c6b28..6043f998bd 100644 --- a/scripts/system/playRecordingAC.js +++ b/scripts/system/playRecordingAC.js @@ -369,7 +369,7 @@ } function onMessageReceived(channel, message, sender) { - if (channel !== HIFI_RECORDER_CHANNEL) { + if (channel !== HIFI_PLAYER_CHANNEL) { return; } From fd919f0db741689ebf0565f99f4431ace71e842b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Apr 2017 10:34:15 +1200 Subject: [PATCH 56/94] Play sounds during countdown and beep when recording starts and finishes --- .../system/assets/sounds/countdown-tick.wav | Bin 0 -> 9702 bytes .../system/assets/sounds/finish-recording.wav | Bin 0 -> 63128 bytes .../system/assets/sounds/start-recording.wav | Bin 0 -> 63128 bytes scripts/system/record.js | 74 +++++++++--------- 4 files changed, 39 insertions(+), 35 deletions(-) create mode 100644 scripts/system/assets/sounds/countdown-tick.wav create mode 100644 scripts/system/assets/sounds/finish-recording.wav create mode 100644 scripts/system/assets/sounds/start-recording.wav diff --git a/scripts/system/assets/sounds/countdown-tick.wav b/scripts/system/assets/sounds/countdown-tick.wav new file mode 100644 index 0000000000000000000000000000000000000000..015e1f642e91b72578bca1c318f7a7c0d515d843 GIT binary patch literal 9702 zcmZ{q=T=m0v##%|XEultF{6N}hzO!!Kru%Y6%`S5{aa(~vHq=p>mc@te6N@z=A5%A ziWoo)h=__Jf_BfU+GEacczwS$_9=8%-Op9mJu5e?Ti22U)U2#t`(OY4Ka*1d5D07b z1OIm?fsu;;^Pm6opJ#Faz=rZfoxvr#hPGon)}q8_@I)EvOuykBJ+&Kj+V;{RJ4bh| ziQ27)ou+b@7Vu(O@74Gfk(IGx&cQgOAcim2!jJR{)!AQkj4sOq*DRmhZ%yJcGQ}0? zGTLlAX`?OS**Y1cIO6w4I{X)W9~bQ~?XeTMP7T&dUFOpe8&A15k5*%wR=F}+$oa_7 z;Y`+zHax>^oW%hgz**d;XY$_r;lmptlPDjHP-a!UO;)-2GTlwk!4T@QR%)Qy!-2oTm$Ttya=@thEK4 zr|CRQjk@t3jktv~*pGuahdXG(2lRx0R3-{gYGqW7tyqP5mP6w$g?xOq*Zf$nx)ZY7 z9o38cKwt7F?V}`&#uS>POL>#*a+`E1&A}8KMM?N&9rOwh>>?l4-E@Mk=_78{FYIdy zkGC9~>sI+&ylN?WZ(ew@KAG~zwF0Sw0^%)~;hLlr8q1`99) z=@<&dH@w9Y+`uX9#bI2qyY7YG?)Cc4OY^h5LVvMWE;U@C%c&UCFxHYes-4t=dcEu& z_y6S`_bhWqE1hsFyz4bGm2eaTt5)lwGGMcxzp( zGQ_1zu3Mn1QOQ-Z)-9Ago@B!q@}1wKQEz%@V*9*%GNNfGiqm%s=%*Fg0`@hiosMwi}d~8X9Kd z)X-Y1r0uqbN^AyAw4pHij(7HiZrUmA<3s+r$lX|z*DgKmFj6z!EGcyxw3@cq3Yu%v zXdF^3N?)vn9$8{LaoHYFvwh;k#ZhhA-WFXXPf zpofB~(d+Jsym8+|+)$aoGq6BcdzG>6k#(_>$jsQp$Z+3Wx4xsqcG>|xr?5uKy9B+IqCCQI|iM0BU+%>6lwQ|H=lKbwJbnq{W&}hr1IkuEG*)G~-e{83A zxL0!DU6LcNR_gegJwhvewHOVyahOi?Y$a{A9azt$lJ7EQgk$|dAM6?3!8toX`|UK{ zLZiJyH-+12d9={ha+Ph1ZzrYO5DI^1YoHr;GQOR3&z_?lKjCPkX7Vg8g

>|!Po6E-f)-rJVeL4Tz_6we;Vd1n{WwdvwUOJb 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 VwQ2KIs$3-ONOGe9gW(vXV1KJl2V?*M literal 0 HcmV?d00001 diff --git a/scripts/system/record.js b/scripts/system/record.js index 3be41e59ac..9500dc3c15 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -21,7 +21,6 @@ button, isConnected, - CountdownTimer, RecordingIndicator, Recorder, Player, @@ -37,37 +36,6 @@ } - CountdownTimer = (function () { - // Counts down a few seconds. - var countdownTimer, - countdownSeconds, - COUNTDOWN_SECONDS = 3, - finishCallback; - - function start(onFinishCallback) { - finishCallback = onFinishCallback; - countdownSeconds = COUNTDOWN_SECONDS; - countdownTimer = Script.setInterval(function () { - countdownSeconds -= 1; - if (countdownSeconds <= 0) { - Script.clearInterval(countdownTimer); - finishCallback(); - } else { - // TODO: Tick. - } - }, 1000); - } - - function cancel() { - Script.clearInterval(countdownTimer); - } - - return { - start: start, - cancel: cancel - }; - }()); - RecordingIndicator = (function () { // Displays "recording" overlay. @@ -139,7 +107,27 @@ mappingPath, startPosition, startOrientation, - play; + play, + + countdownTimer, + countdownSeconds, + COUNTDOWN_SECONDS = 3, + + tickSound, + startRecordingSound, + finishRecordingSound, + TICK_SOUND = "assets/sounds/countdown-tick.wav", + START_RECORDING_SOUND = "assets/sounds/start-recording.wav", + FINISH_RECORDING_SOUND = "assets/sounds/finish-recording.wav", + SOUND_VOLUME = 0.2; + + function playSound(sound) { + Audio.playSound(sound, { + position: MyAvatar.position, + localOnly: true, + volume: SOUND_VOLUME + }); + } function setMappingCallback(status) { if (status !== "") { @@ -174,6 +162,7 @@ function startRecording() { recordingState = RECORDING; log("Start recording"); + playSound(startRecordingSound); startPosition = MyAvatar.position; startOrientation = MyAvatar.orientation; Recording.startRecording(); @@ -186,6 +175,7 @@ recordingState = IDLE; log("Finish recording"); + playSound(finishRecordingSound); Recording.stopRecording(); RecordingIndicator.hide(); success = Recording.saveRecordingToAsset(saveRecordingToAssetCallback); @@ -208,14 +198,24 @@ function cancelCountdown() { recordingState = IDLE; - CountdownTimer.cancel(); + Script.clearInterval(countdownTimer); log("Cancel countdown"); } function startCountdown() { recordingState = COUNTING_DOWN; log("Start countdown"); - CountdownTimer.start(finishCountdown); + playSound(tickSound); + countdownSeconds = COUNTDOWN_SECONDS; + countdownTimer = Script.setInterval(function () { + countdownSeconds -= 1; + if (countdownSeconds <= 0) { + Script.clearInterval(countdownTimer); + finishCountdown(); + } else { + playSound(tickSound); + } + }, 1000); } function isIdle() { @@ -232,6 +232,10 @@ function setUp(playerCallback) { play = playerCallback; + + tickSound = SoundCache.getSound(Script.resolvePath(TICK_SOUND)); + startRecordingSound = SoundCache.getSound(Script.resolvePath(START_RECORDING_SOUND)); + finishRecordingSound = SoundCache.getSound(Script.resolvePath(FINISH_RECORDING_SOUND)); } function tearDown() { From 1d43e5a992e925e638479956965365b8669e45db Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Apr 2017 15:12:36 +1200 Subject: [PATCH 57/94] Style overall dialog layout --- scripts/system/html/css/record.css | 74 ++++++++++++++++++++++++++++++ scripts/system/html/js/record.js | 8 ++++ scripts/system/html/record.html | 19 +++++--- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/scripts/system/html/css/record.css b/scripts/system/html/css/record.css index f9cb76780c..3feae07fb9 100644 --- a/scripts/system/html/css/record.css +++ b/scripts/system/html/css/record.css @@ -8,6 +8,80 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ + +body { + padding: 0; +} + +.title { + padding-left: 21px; +} + +.title label { + font-size: 18px; + position: relative; + top: 12px; +} + + +#recordings { + height: 100%; + position: absolute; + top: 0; + left: 0; + padding: 63px 21px 158px 21px; + box-sizing: border-box; +} + +#recordings table { + height: 100%; + table-layout: fixed; + background: none !important; +} + +#recordings thead, #recordings thead tr { + background: none !important; +} + +#recordings table col#unload-column { + width: 100px; +} + +#recordings table td { + text-overflow: ellipsis; +} + +#recordings table td:nth-child(2) { + text-align: center; +} + +#recordings tbody tr.filler td { + height: auto; + background-color: #2e2e2e; + border-top: 1px solid #1c1c1c; +} + + +#load-button { + margin-top: 14px; +} + + +#record-controls { + position: absolute; + bottom: 7px; + width: 100%; +} + +#record-controls div:first-child { + text-align: center; +} + +#record-controls div.property { + padding-left: 21px; +} + + .hidden { display: none; } diff --git a/scripts/system/html/js/record.js b/scripts/system/html/js/record.js index 20f3e0fb2b..ade8f943c3 100644 --- a/scripts/system/html/js/record.js +++ b/scripts/system/html/js/record.js @@ -96,6 +96,14 @@ function updateRecordings() { tbody.appendChild(tr); } + // Filler row for extra table space. + tr = document.createElement("tr"); + tr.classList.add("filler"); + td = document.createElement("td"); + td.colSpan = 2; + tr.appendChild(td); + tbody.appendChild(tr); + elRecordingsPlaying.replaceChild(tbody, elRecordingsList); elRecordingsList = document.getElementById("recordings-list"); } diff --git a/scripts/system/html/record.html b/scripts/system/html/record.html index cf70fb2401..693040a99e 100644 --- a/scripts/system/html/record.html +++ b/scripts/system/html/record.html @@ -19,9 +19,14 @@

+
+ + + + @@ -62,12 +67,14 @@ -
- -
-
- - +
+
+ +
+
+ + +
Recordings Being Played