mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-18 05:20:50 +01:00
Merge pull request #8725 from nextcloud/add-specific-ui-for-recording-calls
Add specific UI for recording calls
This commit is contained in:
commit
7e0012a63f
10 changed files with 281 additions and 19 deletions
|
|
@ -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],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
97
src/Recording.vue
Normal 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>
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
100
src/mainRecording.js
Normal 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
|
||||
|
|
@ -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
6
templates/recording.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
script('spreed', 'talk-recording');
|
||||
style('spreed', 'icons');
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue