Merge pull request #8725 from nextcloud/add-specific-ui-for-recording-calls

Add specific UI for recording calls
This commit is contained in:
Joas Schilling 2023-02-17 07:24:39 +01:00 committed by GitHub
commit 7e0012a63f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 281 additions and 19 deletions

View file

@ -39,5 +39,7 @@ return [
['name' => 'Page#showCall', 'url' => '/call/{token}', 'root' => '', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PageController::authenticatePassword() */
['name' => 'Page#authenticatePassword', 'url' => '/call/{token}', 'root' => '', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PageController::recording() */
['name' => 'Page#recording', 'url' => '/call/{token}/recording', 'root' => '', 'verb' => 'GET', 'requirements' => $requirements],
],
];

View file

@ -41,6 +41,7 @@ use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
@ -302,6 +303,56 @@ class PageController extends Controller {
return $response;
}
/**
* @PublicPage
* @NoCSRFRequired
* @BruteForceProtection(action=talkRoomToken)
*
* @param string $token
* @return TemplateResponse|NotFoundResponse
*/
public function recording(string $token): Response {
try {
$room = $this->manager->getRoomByToken($token);
} catch (RoomNotFoundException $e) {
$response = new NotFoundResponse();
$response->throttle(['token' => $token]);
return $response;
}
if (class_exists(LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new LoadViewer());
}
$this->publishInitialStateForGuest();
$this->eventDispatcher->dispatchTyped(new LoadAdditionalScriptsEvent());
$this->eventDispatcher->dispatchTyped(new RenderReferenceEvent());
$response = new PublicTemplateResponse($this->appName, 'recording', [
'id-app-content' => '#app-content-vue',
'id-app-navigation' => null,
]);
$response->setFooterVisible(false);
$csp = new ContentSecurityPolicy();
$csp->addAllowedConnectDomain('*');
$csp->addAllowedMediaDomain('blob:');
$csp->addAllowedWorkerSrcDomain('blob:');
$csp->addAllowedWorkerSrcDomain("'self'");
$csp->addAllowedChildSrcDomain('blob:');
$csp->addAllowedChildSrcDomain("'self'");
$csp->addAllowedScriptDomain('blob:');
$csp->addAllowedScriptDomain("'self'");
$csp->addAllowedConnectDomain('blob:');
$csp->addAllowedConnectDomain("'self'");
$csp->addAllowedImageDomain('https://*.tile.openstreetmap.org');
$response->setContentSecurityPolicy($csp);
return $response;
}
/**
* @param string $token
* @param string $password

View file

@ -219,6 +219,9 @@ class SeleniumHelper:
options.set_preference('media.navigator.permission.disabled', True)
# Allow to play media without user interaction.
options.set_preference('media.autoplay.default', 0)
options.add_argument('--kiosk')
options.add_argument(f'--width={width}')
options.add_argument(f'--height={height}')
@ -395,22 +398,7 @@ class Participant():
:param token: the token of the room to join.
"""
self.seleniumHelper.driver.get(self.nextcloudUrl + '/index.php/call/' + token)
# Hack to prevent the participant from using any device that might be
# available, including PulseAudio's "Monitor of Dummy Output".
self.seleniumHelper.execute('navigator.mediaDevices.getUserMedia = async function() { throw new Error() }')
WebDriverWait(self.seleniumHelper.driver, timeout=30).until(lambda driver: driver.find_element(By.CSS_SELECTOR, '.top-bar #call_button:not(:disabled)'))
self.seleniumHelper.driver.find_element(By.CSS_SELECTOR, '.top-bar #call_button').click()
try:
# If the device selector is shown click on the "Join call" button
# in the dialog to actually join the call.
WebDriverWait(self.seleniumHelper.driver, timeout=5).until(lambda driver: driver.find_element(By.CSS_SELECTOR, '.device-checker #call_button'))
self.seleniumHelper.driver.find_element(By.CSS_SELECTOR, '.device-checker #call_button').click()
except:
pass
self.seleniumHelper.driver.get(self.nextcloudUrl + '/index.php/call/' + token + '/recording')
def leaveCall(self):
"""

97
src/Recording.vue Normal file
View file

@ -0,0 +1,97 @@
<!--
- @copyright Copyright (c) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<CallView :token="token"
:is-recording="true" />
</template>
<script>
import {
PARTICIPANT,
} from './constants.js'
import {
joinCall,
} from './services/callsService.js'
import {
leaveConversationSync,
} from './services/participantsService.js'
import {
mediaDevicesManager,
signalingKill,
} from './utils/webrtc/index.js'
import CallView from './components/CallView/CallView.vue'
export default {
name: 'Recording',
components: {
CallView,
},
computed: {
/**
* The current conversation token
*
* @return {string} The token.
*/
token() {
return this.$store.getters.getToken()
},
},
async beforeMount() {
if (this.$route.name === 'recording') {
await this.$store.dispatch('updateToken', this.$route.params.token)
await this.$store.dispatch('setPlaySounds', false)
await this.$store.dispatch('joinConversation', { token: this.token })
mediaDevicesManager.set('audioInputId', null)
mediaDevicesManager.set('videoInputId', null)
await joinCall(this.token, PARTICIPANT.CALL_FLAG.IN_CALL, false)
}
window.addEventListener('unload', () => {
console.info('Navigating away, leaving conversation')
if (this.token) {
// We have to do this synchronously, because in unload and
// beforeunload Promises, async and await are prohibited.
signalingKill()
leaveConversationSync(this.token)
}
})
},
}
</script>
<style lang="scss" scoped>
/* The CallView descendants expect border-box to be set, as in the normal UI the
* CallView is a descendant of NcContent, which applies the border-box to all
* its descendants.
*/
#call-container {
:deep(*) {
box-sizing: border-box;
}
}
</style>

View file

@ -104,6 +104,7 @@
<Grid v-if="!isSidebar"
v-bind="$attrs"
:is-stripe="!isGrid"
:is-recording="isRecording"
:token="token"
:fit-video="true"
:has-pagination="true"
@ -174,6 +175,11 @@ export default {
type: Boolean,
default: false,
},
// Determines whether this component is used in the recording view
isRecording: {
type: Boolean,
default: false,
},
},
data() {

View file

@ -21,7 +21,7 @@
<template>
<div class="grid-main-wrapper" :class="{'is-grid': !isStripe, 'transparent': isLessThanTwoVideos}">
<button v-if="isStripe"
<button v-if="isStripe && !isRecording"
class="stripe--collapse"
:aria-label="stripeButtonTooltip"
@click="handleClickStripeCollapse">
@ -88,7 +88,7 @@
class="dev-mode-video--self video"
:style="{'background': 'url(' + placeholderImage(8) + ')'}" />
</template>
<LocalVideo v-if="!isStripe && !screenshotMode"
<LocalVideo v-if="!isStripe && !isRecording && !screenshotMode"
ref="localVideo"
class="video"
:is-grid="true"
@ -108,7 +108,7 @@
:size="20" />
</button>
</div>
<LocalVideo v-if="isStripe && !screenshotMode"
<LocalVideo v-if="isStripe && !isRecording && !screenshotMode"
ref="localVideo"
class="video"
:is-stripe="true"
@ -243,6 +243,10 @@ export default {
type: Boolean,
default: false,
},
isRecording: {
type: Boolean,
default: false,
},
callParticipantModels: {
type: Array,
required: true,

100
src/mainRecording.js Normal file
View file

@ -0,0 +1,100 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import Recording from './Recording.vue'
// Store
import Vuex from 'vuex'
import store from './store/index.js'
// Router
import VueRouter from 'vue-router'
import router from './router/router.js'
// Utils
import { generateFilePath } from '@nextcloud/router'
import { getRequestToken } from '@nextcloud/auth'
// Directives
import { translate, translatePlural } from '@nextcloud/l10n'
import VueObserveVisibility from 'vue-observe-visibility'
import VueShortKey from 'vue-shortkey'
import vOutsideEvents from 'vue-outside-events'
import { options as TooltipOptions } from '@nextcloud/vue/dist/Directives/Tooltip.js'
// Styles
import '@nextcloud/dialogs/styles/toast.scss'
import 'leaflet/dist/leaflet.css'
// Leaflet icon patch
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css' // Re-uses images from ~leaflet package
// eslint-disable-next-line
import 'leaflet-defaulticon-compatibility'
// CSP config for webpack dynamic chunk loading
// eslint-disable-next-line
__webpack_nonce__ = btoa(getRequestToken())
// Correct the root of the app for chunk loading
// OC.linkTo matches the apps folders
// OC.generateUrl ensure the index.php (or not)
// We do not want the index.php since we're loading files
// eslint-disable-next-line
__webpack_public_path__ = generateFilePath('spreed', '', 'js/')
Vue.prototype.t = translate
Vue.prototype.n = translatePlural
Vue.prototype.OC = OC
Vue.prototype.OCA = OCA
Vue.use(Vuex)
Vue.use(VueRouter)
Vue.use(VueObserveVisibility)
Vue.use(VueShortKey, { prevent: ['input', 'textarea', 'div'] })
Vue.use(vOutsideEvents)
TooltipOptions.container = '#call-container'
store.dispatch('setMainContainerSelector', '#call-container')
window.store = store
if (!window.OCA.Talk) {
window.OCA.Talk = {}
}
const instance = new Vue({
el: '#content',
store,
router,
render: h => h(Recording),
})
// make the instance available to global components that might run on the same page
OCA.Talk.instance = instance
export default instance

View file

@ -23,6 +23,7 @@
import Vue from 'vue'
import Router from 'vue-router'
import { getRootUrl, generateUrl } from '@nextcloud/router'
import CallView from '../components/CallView/CallView.vue'
import MainView from '../views/MainView.vue'
import NotFoundView from '../views/NotFoundView.vue'
import SessionConflictView from '../views/SessionConflictView.vue'
@ -67,5 +68,11 @@ export default new Router({
component: MainView,
props: true,
},
{
path: '/call/:token/recording',
name: 'recording',
component: CallView,
props: true,
},
],
})

6
templates/recording.php Normal file
View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
script('spreed', 'talk-recording');
style('spreed', 'icons');

View file

@ -7,6 +7,7 @@ webpackConfig.entry = {
'admin-settings': path.join(__dirname, 'src', 'mainAdminSettings.js'),
collections: path.join(__dirname, 'src', 'collections.js'),
main: path.join(__dirname, 'src', 'main.js'),
recording: path.join(__dirname, 'src', 'mainRecording.js'),
'files-sidebar': [
path.join(__dirname, 'src', 'mainFilesSidebar.js'),
path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'),