diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
index 18fc16a8..08c0b625 100644
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -1,57 +1,57 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
//import { } from '@fortawesome/free-brands-svg-icons'
import {
faCheckSquare,
faCreditCard,
faSquare,
} from '@fortawesome/free-regular-svg-icons'
import {
faCheck,
faCheckCircle,
- faComments,
+// faComments,
faDownload,
faGlobe,
faExclamationCircle,
faInfoCircle,
faLock,
faKey,
faPlus,
faSearch,
faSignInAlt,
faSyncAlt,
faTrashAlt,
faUser,
faUserCog,
faUsers,
faWallet
} from '@fortawesome/free-solid-svg-icons'
// Register only these icons we need
library.add(
faCheck,
faCheckCircle,
faCheckSquare,
- faComments,
+// faComments,
faCreditCard,
faDownload,
faExclamationCircle,
faGlobe,
faInfoCircle,
faLock,
faKey,
faPlus,
faSearch,
faSignInAlt,
faSquare,
faSyncAlt,
faTrashAlt,
faUser,
faUserCog,
faUsers,
faWallet
)
export default FontAwesomeIcon
diff --git a/src/resources/js/meet.js b/src/resources/js/meet.js
index d34cf76c..108a2ebe 100644
--- a/src/resources/js/meet.js
+++ b/src/resources/js/meet.js
@@ -1,71 +1,89 @@
/**
* Application code for the Meet UI
*/
import routes from './routes-meet.js'
window.routes = routes
require('./bootstrap')
import AppComponent from '../vue/Meet/App'
import MenuComponent from '../vue/Widgets/Menu'
import store from './store'
const app = new Vue({
el: '#app',
components: {
AppComponent,
MenuComponent,
},
store,
router: window.router,
data() {
return {
isLoading: true,
}
},
methods: {
errorPage(code, msg) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
// we can't really use router to display error page as it has two side
// effects: it changes the URL and adds the error page to browser history.
// For now we'll be replacing current view with error page "manually".
const map = {
400: "Bad request",
401: "Unauthorized",
403: "Access denied",
404: "Not found",
405: "Method not allowed",
500: "Internal server error"
}
if (!msg) msg = map[code] || "Unknown Error"
const error_page = `
`
$('#app').children(':not(nav)').remove()
$('#app').append(error_page)
},
errorHandler(error) {
if (!error.response) {
// TODO: probably network connection error
} else {
this.errorPage(error.response.status, error.response.statusText)
}
}
}
})
// Add a axios request interceptor
window.axios.interceptors.request.use(
config => {
// We're connecting to the API on the main domain
config.url = window.config['app.url'] + config.url
return config
},
error => {
// Do something with request error
return Promise.reject(error)
}
)
+
+// Register additional icons
+import { library } from '@fortawesome/fontawesome-svg-core'
+
+import {
+ faExpand,
+ faMicrophone,
+ faPowerOff,
+ faVideo
+} from '@fortawesome/free-solid-svg-icons'
+
+// Register only these icons we need
+library.add(
+ faExpand,
+ faMicrophone,
+ faPowerOff,
+ faVideo
+)
diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js
index 3e723efb..c397277d 100644
--- a/src/resources/js/meet/app.js
+++ b/src/resources/js/meet/app.js
@@ -1,177 +1,229 @@
import { OpenVidu } from 'openvidu-browser'
function Meet(container)
{
let OV // OpenVidu object to initialize a session
let session // Session object where the user will connect
let publisher // Publisher object which the user will publish
let sessionId // Unique identifier of the session
let audioEnabled = true // True if the audio track of publisher is active
let videoEnabled = true // True if the video track of publisher is active
let numOfVideos = 0 // Keeps track of the number of videos that are being shown
+ let audioSource = '' // Currently selected microphone
+ let videoSource = '' // Currently selected camera
+
+ let publisherDefaults = {
+ publishAudio: true, // Whether to start publishing with your audio unmuted or not
+ publishVideo: true, // Whether to start publishing with your video enabled or not
+ resolution: '640x480', // The resolution of your video
+ frameRate: 30, // The frame rate of your video
+ mirror: true // Whether to mirror your local video or not
+ }
- $(container).append('')
+ let cameras = [] // List of user video devices
+ let microphones = [] // List of user audio devices
OV = new OpenVidu()
// if there's anything to do, do it here.
//OV.setAdvancedConfiguration(config)
// Disconnect participant on browser's window closed
/*
window.addEventListener('beforeunload', () => {
if (session) session.disconnect();
})
*/
// Public methods
this.joinRoom = joinRoom
+ this.leaveRoom = leaveRoom
+ this.muteAudio = muteAudio
+ this.muteVideo = muteVideo
+ this.setup = setup
+ this.setupSetAudioDevice = setupSetAudioDevice
+ this.setupSetVideoDevice = setupSetVideoDevice
+
+
+ function setup(videoElement, success_callback, error_callback) {
+ publisher = OV.initPublisher(null, publisherDefaults)
+
+ publisher.once('accessDenied', error => {
+ error_callback(error)
+ })
+
+ publisher.once('accessAllowed', async () => {
+ let mediaStream = publisher.stream.getMediaStream()
+ let videoStream = mediaStream.getVideoTracks()[0]
+ let audioStream = mediaStream.getAudioTracks()[0]
+
+ audioEnabled = !!audioStream
+ videoEnabled = !!videoStream
+
+ publisher.addVideoElement(videoElement)
+
+ const devices = await OV.getDevices()
+
+ devices.forEach(device => {
+ // device's props: deviceId, kind, label
+ if (device.kind == 'videoinput') {
+ cameras.push(device)
+ if (videoStream && videoStream.label == device.label) {
+ videoSource = device.deviceId
+ }
+ } else if (device.kind == 'audioinput') {
+ microphones.push(device)
+ if (audioStream && audioStream.label == device.label) {
+ audioSource = device.deviceId
+ }
+ }
+ })
+
+ success_callback({
+ microphones,
+ cameras,
+ audioSource,
+ videoSource,
+ audioEnabled,
+ videoEnabled
+ })
+ })
+ }
+
+ async function setupSetAudioDevice(deviceId) {
+ if (!deviceId) {
+ publisher.publishAudio(false)
+ audioEnabled = false
+ } else if (deviceId == audioSource) {
+ publisher.publishAudio(true)
+ audioEnabled = true
+ } else {
+/*
+ let mediaStream = publisher.stream.getMediaStream()
+ let audioStream = mediaStream.getAudioTracks()[0]
+
+ audioStream.stop()
+
+ publisher = OV.initPublisher(null, properties);
+ publisher.addVideoElement(videoElement)
+*/
+
+ // FIXME: None of this is working
+
+ let properties = Object.assign({}, publisherDefaults, {
+ publishAudio: true,
+ publishVideo: videoEnabled,
+ audioSource: deviceId,
+ videoSource: videoSource
+ })
+
+ await OV.getUserMedia(properties)
+ .then(async (mediaStream) => {
+ const track = mediaStream.getAudioTracks()[0]
+ await publisher.replaceTrack(track)
+ audioEnabled = true
+ })
+ }
+
+ return audioEnabled
+ }
+
+ function setupSetVideoDevice(deviceId) {
+ if (!deviceId) {
+ publisher.publishVideo(false)
+ videoEnabled = false
+ } else if (deviceId == videoSource) {
+ publisher.publishVideo(true)
+ videoEnabled = true
+ } else {
+ // TODO
+ }
+
+ return videoEnabled
+ }
function joinRoom(data) {
sessionId = data.session
// Init a session
session = OV.initSession()
// On every new Stream received...
session.on('streamCreated', function (event) {
- // Subscribe to the Stream to receive it. HTML video will be appended to element with 'subscriber' id
- var subscriber = session.subscribe(event.stream, 'videos');
+ // Subscribe to the Stream to receive it
+ let subscriber = session.subscribe(event.stream, addVideoWrapper(container));
+
// When the new video is added to DOM, update the page layout to fit one more participant
subscriber.on('videoElementCreated', (event) => {
numOfVideos++
updateLayout()
})
})
// On every new Stream destroyed...
session.on('streamDestroyed', (event) => {
// Update the page layout
numOfVideos--
updateLayout()
})
+ // TODO
+ let params = {
+ clientData: 'Test', // user nickname
+ avatar: undefined // avatar image
+ }
+
// Connect with the token
- session.connect(data.token)
+ session.connect(data.token, params)
.then(() => {
- // Update the URL shown in the browser's navigation bar to show the session id
- ///var path = (location.pathname.slice(-1) == "/" ? location.pathname : location.pathname + "/");
- ///window.history.pushState("", "", path + '#' + sessionId);
-
- // Auxiliary methods to show the session's view
- //showSessionHideJoin()
-
- // Get the camera stream with the desired properties
- publisher = OV.initPublisher('videos', {
- audioSource: undefined, // The source of audio. If undefined default audio input
- videoSource: undefined, // The source of video. If undefined default video input
- publishAudio: true, // Whether to start publishing with your audio unmuted or not
- publishVideo: true, // Whether to start publishing with your video enabled or not
- resolution: '640x480', // The resolution of your video
- frameRate: 30, // The frame rate of your video
- insertMode: 'PREPEND', // How the video is inserted in target element 'video-container'
- mirror: true // Whether to mirror your local video or not
- })
+ publisher.createVideoElement(addVideoWrapper(container), 'PREPEND')
// When our HTML video has been added to DOM...
publisher.on('videoElementCreated', (event) => {
+ $(event.element).addClass('publisher')
+ .prop('muted', true) // Mute local video to avoid feedback
+
// When your own video is added to DOM, update the page layout to fit it
numOfVideos++
updateLayout()
- $(event.element).prop('muted', true) // Mute local video to avoid feedback
})
// Publish the stream
session.publish(publisher)
})
.catch(error => {
console.log('There was an error connecting to the session:', error.code, error.message);
})
}
function leaveRoom() {
// Leave the session by calling 'disconnect' method over the Session object
- session.disconnect();
+ if (session) {
+ session.disconnect();
+ }
}
function muteAudio() {
audioEnabled = !audioEnabled
publisher.publishAudio(audioEnabled)
- if (!audioEnabled) {
- $('#mute-audio').removeClass('btn-primary')
- $('#mute-audio').addClass('btn-default')
- } else {
- $('#mute-audio').addClass('btn-primary')
- $('#mute-audio').removeClass('btn-default')
- }
+ return audioEnabled
}
function muteVideo() {
videoEnabled = !videoEnabled
publisher.publishVideo(videoEnabled)
- if (!videoEnabled) {
- $('#mute-video').removeClass('btn-primary')
- $('#mute-video').addClass('btn-default')
- } else {
- $('#mute-video').addClass('btn-primary')
- $('#mute-video').removeClass('btn-default')
- }
+ return videoEnabled
}
- // 'Session' page
- function showSessionHideJoin() {
- $('#nav-join').hide()
- $('#nav-session').show()
- $('#join').hide()
- $('#session').show()
- $('footer').hide()
- $('#main-container').removeClass('container')
- }
-
- // 'Join' page
- function showJoinHideSession() {
- $('#nav-join').show()
- $('#nav-session').hide()
- $('#join').show()
- $('#session').hide()
- $('footer').show()
- $('#main-container').addClass('container')
+ function updateLayout() {
+ // update the "matrix" layout
}
- // Dynamic layout adjustemnt depending on number of videos
- function updateLayout() {
- console.warn('There are now ' + numOfVideos + ' videos')
-
- var publisherDiv = $('#publisher')
- var publisherVideo = $("#publisher video")
- var subscriberVideos = $('#videos > video')
-
- switch (numOfVideos) {
- case 1:
- publisherVideo.addClass('video1')
- break
- case 2:
- publisherDiv.addClass('video2')
- subscriberVideos.addClass('video2')
- break
- case 3:
- publisherDiv.addClass('video3')
- subscriberVideos.addClass('video3')
- break
- case 4:
- publisherDiv.addClass('video4')
- publisherVideo.addClass('video4')
- subscriberVideos.addClass('video4')
- break
- default:
- publisherDiv.addClass('videoMore')
- publisherVideo.addClass('videoMore')
- subscriberVideos.addClass('videoMore')
- break
- }
+ function addVideoWrapper(container) {
+ return $('').appendTo(container).get(0)
}
}
export default Meet
diff --git a/src/resources/js/routes-meet.js b/src/resources/js/routes-meet.js
index d1e95c01..b3dc342f 100644
--- a/src/resources/js/routes-meet.js
+++ b/src/resources/js/routes-meet.js
@@ -1,15 +1,16 @@
import DashboardComponent from '../vue/Meet/Dashboard'
import RoomComponent from '../vue/Meet/Room'
const routes = [
{
path: '/',
+ name: 'dashboard',
component: DashboardComponent
},
{
path: '*',
component: RoomComponent
}
]
export default routes
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
index d6e698c6..abc7a5cc 100644
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -1,352 +1,353 @@
@import 'variables';
@import 'bootstrap';
+@import 'meet';
@import 'menu';
@import 'toast';
@import 'forms';
html,
body,
body > .outer-container {
height: 100%;
}
#app {
display: flex;
flex-direction: column;
min-height: 100%;
& > nav {
flex-shrink: 0;
z-index: 12;
}
& > div.container {
flex-grow: 1;
margin-top: 2rem;
margin-bottom: 2rem;
}
& > .filler {
flex-grow: 1;
}
& > div.container + .filler {
display: none;
}
}
#error-page {
position: absolute;
top: 0;
height: 100%;
width: 100%;
align-items: center;
display: flex;
justify-content: center;
color: #636b6f;
z-index: 10;
background: white;
.code {
text-align: right;
border-right: 2px solid;
font-size: 26px;
padding: 0 15px;
}
.message {
font-size: 18px;
padding: 0 15px;
}
}
.app-loader {
background-color: $body-bg;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 8;
.spinner-border {
width: 120px;
height: 120px;
border-width: 15px;
color: #b2aa99;
}
&.small .spinner-border {
width: 25px;
height: 25px;
border-width: 3px;
}
&.fadeOut {
visibility: hidden;
opacity: 0;
transition: visibility 400ms linear, opacity 400ms linear;
}
}
pre {
margin: 1rem 0;
padding: 1rem;
background-color: $menu-bg-color;
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
}
tfoot.table-fake-body {
background-color: #f8f8f8;
color: grey;
text-align: center;
td {
vertical-align: middle;
height: 8em;
}
tbody:not(:empty) + & {
display: none;
}
}
table {
td.buttons,
td.email,
td.price,
td.datetime,
td.selection {
width: 1%;
white-space: nowrap;
}
th.price,
td.price {
width: 1%;
text-align: right;
white-space: nowrap;
}
&.form-list {
margin: 0;
td {
border: 0;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
button {
line-height: 1;
}
}
.btn-action {
line-height: 1;
padding: 0;
}
}
.list-details {
min-height: 1em;
& > ul {
margin: 0;
padding-left: 1.2em;
}
}
.plan-selector {
.plan-ico {
font-size: 3.8rem;
color: #f1a539;
border: 3px solid #f1a539;
width: 6rem;
height: 6rem;
margin-bottom: 1rem;
border-radius: 50%;
}
}
.plan-description {
& > ul {
padding-left: 1.2em;
&:last-child {
margin-bottom: 0;
}
}
}
#status-box {
background-color: lighten($green, 35);
.progress {
background-color: #fff;
height: 10px;
}
.progress-label {
font-size: 0.9em;
}
.progress-bar {
background-color: $green;
}
&.process-failed {
background-color: lighten($orange, 30);
.progress-bar {
background-color: $red;
}
}
}
#dashboard-nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
& > a {
padding: 1rem;
text-align: center;
white-space: nowrap;
margin: 0.25rem;
text-decoration: none;
width: 150px;
&.disabled {
pointer-events: none;
opacity: 0.6;
}
.badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
}
svg {
width: 6rem;
height: 6rem;
margin: auto;
}
}
.form-separator {
position: relative;
margin: 1em 0;
display: flex;
justify-content: center;
hr {
border-color: #999;
margin: 0;
position: absolute;
top: 0.75em;
width: 100%;
}
span {
background: #fff;
padding: 0 1em;
z-index: 1;
}
}
// Various improvements for mobile
@include media-breakpoint-down(sm) {
.card {
border: 0;
}
.card-body {
padding: 0.5rem 0;
}
.form-group {
margin-bottom: 0.5rem;
}
.tab-content {
margin-top: 0.5rem;
}
.col-form-label {
color: #666;
font-size: 95%;
}
.form-group.plaintext .col-form-label {
padding-bottom: 0;
}
form.read-only.short label {
width: 35%;
& + * {
width: 65%;
}
}
#app > div.container {
margin-bottom: 1rem;
margin-top: 1rem;
}
#header-menu-navbar {
padding: 0;
}
#dashboard-nav > a {
width: 135px;
}
.table-sm:not(.form-list) {
tbody td {
padding: 0.75rem 0.5rem;
svg {
vertical-align: -0.175em;
}
& > svg {
font-size: 125%;
margin-right: 0.25rem;
}
}
}
}
// Openvidu webcomponent improvements
mat-sidenav-container {
z-index: 10 !important;
}
#dialogChooseRoom {
top: 0;
left: 0;
z-index: 10;
background: #fff;
& > .mat-card {
max-height: unset;
margin: 2em auto;
}
}
diff --git a/src/resources/sass/meet.scss b/src/resources/sass/meet.scss
new file mode 100644
index 00000000..8464f659
--- /dev/null
+++ b/src/resources/sass/meet.scss
@@ -0,0 +1,42 @@
+#meet-component {
+
+}
+
+#meet-session-toolbar {
+ display: flex;
+ justify-content: center;
+}
+
+#meet-session-menu {
+ button {
+ font-size: 1.3rem;
+ padding: 0.5rem 1rem;
+ }
+}
+
+#meet-session {
+ display: flex;
+ justify-content: center;
+}
+
+.meet-video {
+ position: relative;
+
+ video {
+ display: block;
+ width: 100%;
+ }
+}
+
+#meet-setup {
+}
+
+#setup-preview {
+ display: flex;
+
+ video {
+ width: 100%;
+ transform: rotateY(180deg);
+ background: black;
+ }
+}
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
index 2aedc7a6..f5490161 100644
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -1,71 +1,68 @@
Your profile
Domains
User accounts
Wallet
{{ $root.price(balance) }}
-
- Video Chat
-
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
index 697bfd7f..d46caf90 100644
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -1,35 +1,141 @@
-
-
+
+
+
+
+
+
+
+
+
+
Set up your session
+
+
+