TT#44287 Conferencing - display the list of participants

- list of participants as column (subtask #67600)
- participant displayName added (subtask #67605)
- participants list scrollable (subtask #67601)
- self participant item on top of the list (subtask #67602)
- self participant fixed position on top of the list (subtask #67603)
- placeholder icon if video not shared by participant (subtask #67309)
- show/hear participants (subtask #67604)

Change-Id: I44836b67767d243554e674e76486681865931f84
changes/90/33890/19
Carlo Venusino 6 years ago committed by Hans-Peter Herzog
parent 37909a6bcc
commit 986d954a77

@ -43,7 +43,6 @@
], ],
data () { data () {
return { return {
currentStream: null,
loading: true, loading: true,
mediaHeight: 0, mediaHeight: 0,
mediaWidth: 0, mediaWidth: 0,
@ -57,6 +56,10 @@
this.$root.$on('window-resized', fitMedia); this.$root.$on('window-resized', fitMedia);
this.$root.$on('content-resized', fitMedia); this.$root.$on('content-resized', fitMedia);
this.$root.$on('orientation-changed', fitMedia); this.$root.$on('orientation-changed', fitMedia);
this.$refs.media.addEventListener('playing', ()=>{
this.loading = false;
this.fitMedia();
});
}, },
components: { components: {
QSpinnerDots, QSpinnerDots,
@ -64,41 +67,19 @@
}, },
methods: { methods: {
assignStream(stream) { assignStream(stream) {
if(stream !== this.currentStream && stream !== null && stream !== undefined) { if(stream !== null && stream !== undefined) {
this.loading = true; this.loading = true;
this.currentStream = stream; if(_.isObject(stream) && _.isObject(this.$refs.media) &&
if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.srcObject)) { !_.isUndefined(this.$refs.media.srcObject)) {
this.$refs.media.srcObject = this.currentStream; this.$refs.media.srcObject = stream;
} }
else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) && else if(_.isObject(stream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.mozSrcObject)) { !_.isUndefined(this.$refs.media.mozSrcObject)) {
this.$refs.media.mozSrcObject = this.currentStream; this.$refs.media.mozSrcObject = stream;
} }
else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) && else if(_.isObject(stream) && _.isObject(this.$refs.media) &&
_.isObject(URL) && _.isFunction(URL.createObjectURL)) { _.isObject(URL) && _.isFunction(URL.createObjectURL)) {
this.$refs.media.src = URL.createObjectURL(this.currentStream); this.$refs.media.src = URL.createObjectURL(stream);
}
let timer = setInterval(()=>{
if(this.currentStream !== null && (this.$refs.media && (this.$refs.media.currentTime > 0 ||
this.$refs.media.readyState > 2))) {
this.loading = false;
clearInterval(timer);
this.fitMedia();
}
}, 100);
}
else {
this.loading = false;
this.currentStream = null;
if(this.$refs.media.srcObject) {
this.$refs.media.srcObject = null;
}
else if(this.$refs.media.mozSrcObject) {
this.$refs.media.mozSrcObject = null;
}
else {
this.$refs.media.src = null;
} }
} }
}, },
@ -192,10 +173,6 @@
} }
}, },
computed: { computed: {
hasVideo() {
return this.currentStream !== null && _.isArray(this.currentStream.getVideoTracks()) &&
this.currentStream.getVideoTracks().length > 0;
},
componentClasses(){ componentClasses(){
return ['csc-media']; return ['csc-media'];
}, },

@ -143,8 +143,7 @@
'localMediaStream', 'localMediaStream',
'participantsList', 'participantsList',
'remoteMediaStream', 'remoteMediaStream',
'remoteMediaStreams', 'remoteMediaStreams'
'hasRemoteMediaStream'
]), ]),
microphoneButtonColor() { microphoneButtonColor() {
if(this.isMicrophoneEnabled) { if(this.isMicrophoneEnabled) {

@ -14,7 +14,6 @@
class="csc-media-cont" class="csc-media-cont"
:muted="true" :muted="true"
:stream="localMediaStream" :stream="localMediaStream"
:preview="true"
/> />
<q-card-title <q-card-title
class="csc-conf-participants-item-title" class="csc-conf-participants-item-title"
@ -26,7 +25,6 @@
<script> <script>
import { QCard, QCardMedia, QCardTitle } from 'quasar-framework' import { QCard, QCardMedia, QCardTitle } from 'quasar-framework'
// import { mapGetters } from 'vuex'
import CscMedia from "../../CscMedia"; import CscMedia from "../../CscMedia";
export default { export default {
name: 'csc-conference-local-participant', name: 'csc-conference-local-participant',

@ -13,9 +13,15 @@
/> />
<div <div
id="csc-conf-remote-participants-cont" id="csc-conf-remote-participants-cont"
v-for="participantId in participantsList" :key="participantId"> v-for="participantId in participantsList"
:key="participantId"
>
<csc-conference-remote-participant <csc-conference-remote-participant
:participant-id="participantId" :key="participantId"
:remote-participant="remoteParticipant(participantId)"
:has-remote-video="hasRemoteVideo(participantId)"
:remote-media-stream="remoteMediaStream"
:remote-media-streams="remoteMediaStreams"
/> />
</div> </div>
</div> </div>
@ -23,7 +29,10 @@
<script> <script>
import {QCard, QCardMedia, QCardTitle} from 'quasar-framework' import {QCard, QCardMedia, QCardTitle} from 'quasar-framework'
import {mapGetters} from 'vuex' import {
mapGetters,
mapState,
} from 'vuex'
import CscMedia from "../../CscMedia"; import CscMedia from "../../CscMedia";
import CscConferenceRemoteParticipant from './CscConferenceRemoteParticipant' import CscConferenceRemoteParticipant from './CscConferenceRemoteParticipant'
import CscConferenceLocalParticipant from './CscConferenceLocalParticipant' import CscConferenceLocalParticipant from './CscConferenceLocalParticipant'
@ -39,13 +48,19 @@
CscConferenceLocalParticipant CscConferenceLocalParticipant
}, },
computed: { computed: {
...mapState('conference', [
'remoteMediaStreams'
]),
...mapGetters('conference', [ ...mapGetters('conference', [
'participantsList', 'participantsList',
'localParticipant', 'localParticipant',
'localMediaStream', 'localMediaStream',
'isMicrophoneEnabled', 'isMicrophoneEnabled',
'isCameraEnabled', 'isCameraEnabled',
'isScreenEnabled' 'isScreenEnabled',
'remoteParticipant',
'remoteMediaStream',
'hasRemoteVideo'
]) ])
}, },
mounted() { mounted() {

@ -1,54 +1,60 @@
<template> <template>
<q-card <q-card
class="csc-conf-participant-cont" class="csc-conf-participant-cont"
> >
<q-card-media <q-card-media
class="csc-avatar-cont" class="csc-avatar-cont"
v-show="!hasRemoteMediaStream(participantId)" v-show="!hasRemoteVideo"
> >
<img src="/statics/avatar.png"> <img src="/statics/avatar.png">
</q-card-media> </q-card-media>
<csc-media <csc-media
v-show="hasRemoteMediaStream(participantId)" v-show="hasRemoteVideo"
class="csc-media-cont" class="csc-media-cont"
ref="{{participantId}}" ref="cscMedia"
:muted="false" :muted="false"
:stream="remoteMediaStream(participantId)" :preview="true"
:preview="true" />
/> <q-card-title
<q-card-title class="csc-conf-participants-item-title"
class="csc-conf-participants-item-title" >
> {{ remoteParticipant.displayName }}
{{remoteParticipant(participantId).displayName}} </q-card-title>
</q-card-title>
</q-card> </q-card>
</template> </template>
<script> <script>
import { QCard, QCardMedia, QCardTitle } from 'quasar-framework' import {QCard, QCardMedia, QCardTitle} from 'quasar-framework'
import { mapGetters } from 'vuex'
import CscMedia from "../../CscMedia"; import CscMedia from "../../CscMedia";
export default { export default {
name: 'csc-conference-remote-participant', name: 'csc-conference-remote-participant',
components: { components: {
QCard, QCard,
QCardMedia, QCardMedia,
QCardTitle, QCardTitle,
CscMedia CscMedia
}, },
props:['participantId'], props: [
computed: {
...mapGetters('conference', [
'remoteParticipant', 'remoteParticipant',
'remoteMediaStream', 'remoteMediaStream',
'hasRemoteMediaStream' 'remoteMediaStreams',
]) 'hasRemoteVideo',
],
mounted() {
this.assignStream();
},
methods: {
assignStream() {
if (this.$refs.cscMedia && this.remoteMediaStreams[this.remoteParticipant.id] === this.remoteParticipant.id) {
this.$refs.cscMedia.assignStream(this.remoteMediaStream(this.remoteParticipant.id));
}
}
}, },
mounted(){ watch: {
// workaround to retrigger :stream binding on csc-media component remoteMediaStreams() {
if(this.hasRemoteMediaStream(this.participantId)){ this.assignStream();
this.$store.commit('conference/addRemoteMedia', this.participantId); }
}
} }
} }
</script> </script>

@ -1,4 +1,3 @@
import Vue from 'vue' import Vue from 'vue'
import { import {
RequestState RequestState
@ -26,7 +25,7 @@ export default {
leaveState: RequestState.initiated, leaveState: RequestState.initiated,
leaveError: null, leaveError: null,
participants: [], participants: [],
remoteMediaStreams: [] remoteMediaStreams: {}
}, },
getters: { getters: {
username(state, getters, rootState, rootGetters) { username(state, getters, rootState, rootGetters) {
@ -70,7 +69,7 @@ export default {
state.localMediaState === RequestState.requesting) && Vue.$conference.hasLocalMediaStream(); state.localMediaState === RequestState.requesting) && Vue.$conference.hasLocalMediaStream();
}, },
localMediaStream(state) { localMediaStream(state) {
if((state.localMediaState === RequestState.succeeded || if ((state.localMediaState === RequestState.succeeded ||
state.localMediaState === RequestState.requesting) && Vue.$conference.hasLocalMediaStream()) { state.localMediaState === RequestState.requesting) && Vue.$conference.hasLocalMediaStream()) {
return Vue.$conference.getLocalMediaStreamNative(); return Vue.$conference.getLocalMediaStreamNative();
} }
@ -81,31 +80,34 @@ export default {
state.localMediaState === RequestState.requesting) && Vue.$conference.hasLocalMediaStream(); state.localMediaState === RequestState.requesting) && Vue.$conference.hasLocalMediaStream();
}, },
localParticipant(state) { localParticipant(state) {
if(state.joinState === RequestState.succeeded){ if (state.joinState === RequestState.succeeded) {
return Vue.$conference.getLocalParticipant(); return Vue.$conference.getLocalParticipant();
} }
}, },
remoteParticipant: () => (participantId) => { remoteParticipant: () => (participantId) => {
return Vue.$conference.getRemoteParticipant(participantId); return Vue.$conference.getRemoteParticipant(participantId);
}, },
remoteMediaStream: (state) => (participantId) => { remoteMediaStream: () => (participantId) => {
if(state.remoteMediaStreams.includes(participantId)){ const participant = Vue.$conference.getRemoteParticipant(participantId);
const participant = Vue.$conference.getRemoteParticipant(participantId); if(participant !== null) {
return participant.mediaStream ? participant.mediaStream.getStream() : null; return participant.mediaStream ? participant.mediaStream.getStream() : null;
} }
return null; return null;
}, },
participantsList(state) { participantsList(state) {
return state.participants; return state.participants;
}, },
remoteMediaStreams(state) { remoteMediaStreams(state) {
return state.remoteMediaStreams; return state.remoteMediaStreams;
}, },
hasRemoteMediaStream: (state) => (participantId) => { hasRemoteVideo: () => (participantId) => {
return state.remoteMediaStreams.includes(participantId) const participant = Vue.$conference.getRemoteParticipant(participantId);
if(participant !== null) {
return participant.mediaStream ? participant.mediaStream.hasVideo() : false;
}
return false;
} }
}, },
mutations: { mutations: {
enableConferencing(state) { enableConferencing(state) {
@ -154,18 +156,10 @@ export default {
state.screenEnabled = false; state.screenEnabled = false;
}, },
addRemoteMedia(state, participantId) { addRemoteMedia(state, participantId) {
if(state.remoteMediaStreams.includes(participantId)){ Vue.set(state.remoteMediaStreams, participantId, participantId);
state.remoteMediaStreams = state.remoteMediaStreams.filter(($participant)=>{
return participantId !== $participant;
});
}
state.remoteMediaStreams.push(participantId);
}, },
removeRemoteMedia(state, participant) { removeRemoteMedia(state, participantId) {
state.remoteMediaStreams = state.remoteMediaStreams.filter(($participant)=>{ Vue.delete(state.remoteMediaStreams, participantId);
return participant !== $participant;
});
}, },
joinRequesting(state) { joinRequesting(state) {
state.joinState = RequestState.requesting; state.joinState = RequestState.requesting;
@ -196,24 +190,26 @@ export default {
state.leaveError = error; state.leaveError = error;
}, },
participantJoined(state, participant) { participantJoined(state, participant) {
if(state.participants.includes(participant.getId())){ if (state.participants.includes(participant.getId())) {
state.participants = state.participants.filter(($participant)=>{ state.participants = state.participants.filter(($participant) => {
return participant.getId() !== $participant; return participant.getId() !== $participant;
}); });
} }
state.participants.push(participant.getId()) state.participants.push(participant.getId());
}, },
participantLeft(state, participant) { participantLeft(state, participant) {
state.participants = state.participants.filter(($participant)=>{ state.participants = state.participants.filter(($participant) => {
return participant.getId() !== $participant; return participant.getId() !== $participant;
}); });
Vue.delete(state.remoteMediaStreams, participant.getId());
} }
}, },
actions: { actions: {
createLocalMedia(context, type) { createLocalMedia(context, type) {
let media = Vue.$rtcEngine.createMedia(); let media = Vue.$rtcEngine.createMedia();
context.commit('localMediaRequesting'); context.commit('localMediaRequesting');
switch(type) { switch (type) {
default: default:
case MediaTypes.mic: case MediaTypes.mic:
media.enableMicrophone(); media.enableMicrophone();
@ -234,13 +230,13 @@ export default {
break; break;
} }
let localMediaStream; let localMediaStream;
return media.build().then(($localMediaStream)=>{ return media.build().then(($localMediaStream) => {
localMediaStream = $localMediaStream; localMediaStream = $localMediaStream;
localMediaStream.onVideoEnded(()=>{ localMediaStream.onVideoEnded(() => {
context.dispatch('createLocalMedia', MediaTypes.mic); context.dispatch('createLocalMedia', MediaTypes.mic);
}); });
Vue.$conference.setLocalMediaStream(localMediaStream); Vue.$conference.setLocalMediaStream(localMediaStream);
switch(type) { switch (type) {
default: default:
case MediaTypes.mic: case MediaTypes.mic:
context.commit('enableMicrophone'); context.commit('enableMicrophone');
@ -269,43 +265,43 @@ export default {
break; break;
} }
return Promise.resolve(); return Promise.resolve();
}).then(()=>{ }).then(() => {
if(context.getters.isJoined) { if (context.getters.isJoined) {
return Vue.$conference.changeConferenceMedia(); return Vue.$conference.changeConferenceMedia();
} }
else { else {
return Promise.resolve(); return Promise.resolve();
} }
}).then(()=>{ }).then(() => {
context.commit('localMediaSucceeded', localMediaStream); context.commit('localMediaSucceeded', localMediaStream);
}).catch((err)=>{ }).catch((err) => {
if(!context.getters.hasLocalMediaStream) { if (!context.getters.hasLocalMediaStream) {
context.commit('localMediaFailed', err.message); context.commit('localMediaFailed', err.message);
} }
}); });
}, },
async enableMicrophone(context) { async enableMicrophone(context) {
if(!context.getters.isLocalMediaRequesting) { if (!context.getters.isLocalMediaRequesting) {
let mediaType = MediaTypes.mic; let mediaType = MediaTypes.mic;
if(context.getters.isCameraEnabled) { if (context.getters.isCameraEnabled) {
mediaType = MediaTypes.micCam; mediaType = MediaTypes.micCam;
} }
else if(context.getters.isScreenEnabled) { else if (context.getters.isScreenEnabled) {
mediaType = MediaTypes.micScreen; mediaType = MediaTypes.micScreen;
} }
await context.dispatch('createLocalMedia', mediaType); await context.dispatch('createLocalMedia', mediaType);
} }
}, },
disableMicrophone(context) { disableMicrophone(context) {
if(!context.getters.isLocalMediaRequesting) { if (!context.getters.isLocalMediaRequesting) {
let mediaType = null; let mediaType = null;
if(context.getters.isCameraEnabled) { if (context.getters.isCameraEnabled) {
mediaType = MediaTypes.cam; mediaType = MediaTypes.cam;
} }
else if(context.getters.isScreenEnabled) { else if (context.getters.isScreenEnabled) {
mediaType = MediaTypes.screen; mediaType = MediaTypes.screen;
} }
if(mediaType === null) { if (mediaType === null) {
context.commit('disposeLocalMedia'); context.commit('disposeLocalMedia');
} }
else { else {
@ -314,7 +310,7 @@ export default {
} }
}, },
toggleMicrophone(context) { toggleMicrophone(context) {
if(!context.getters.isMicrophoneEnabled) { if (!context.getters.isMicrophoneEnabled) {
context.dispatch('enableMicrophone'); context.dispatch('enableMicrophone');
} }
else { else {
@ -322,17 +318,17 @@ export default {
} }
}, },
enableCamera(context) { enableCamera(context) {
if(!context.getters.isLocalMediaRequesting) { if (!context.getters.isLocalMediaRequesting) {
context.dispatch('createLocalMedia', MediaTypes.micCam); context.dispatch('createLocalMedia', MediaTypes.micCam);
} }
}, },
disableCamera(context) { disableCamera(context) {
if(!context.getters.isLocalMediaRequesting) { if (!context.getters.isLocalMediaRequesting) {
let mediaType = null; let mediaType = null;
if(context.getters.isMicrophoneEnabled) { if (context.getters.isMicrophoneEnabled) {
mediaType = MediaTypes.mic; mediaType = MediaTypes.mic;
} }
if(mediaType === null) { if (mediaType === null) {
context.commit('disposeLocalMedia'); context.commit('disposeLocalMedia');
} }
else { else {
@ -341,7 +337,7 @@ export default {
} }
}, },
toggleCamera(context) { toggleCamera(context) {
if(!context.getters.isCameraEnabled) { if (!context.getters.isCameraEnabled) {
context.dispatch('enableCamera'); context.dispatch('enableCamera');
} }
else { else {
@ -349,17 +345,17 @@ export default {
} }
}, },
enableScreen(context) { enableScreen(context) {
if(!context.getters.isLocalMediaRequesting) { if (!context.getters.isLocalMediaRequesting) {
context.dispatch('createLocalMedia', MediaTypes.micScreen); context.dispatch('createLocalMedia', MediaTypes.micScreen);
} }
}, },
disableScreen(context) { disableScreen(context) {
if(!context.getters.isLocalMediaRequesting) { if (!context.getters.isLocalMediaRequesting) {
let mediaType = null; let mediaType = null;
if(context.getters.isMicrophoneEnabled) { if (context.getters.isMicrophoneEnabled) {
mediaType = MediaTypes.mic; mediaType = MediaTypes.mic;
} }
if(mediaType === null) { if (mediaType === null) {
context.commit('disposeLocalMedia'); context.commit('disposeLocalMedia');
} }
else { else {
@ -368,7 +364,7 @@ export default {
} }
}, },
toggleScreen(context) { toggleScreen(context) {
if(!context.getters.isScreenEnabled) { if (!context.getters.isScreenEnabled) {
context.dispatch('enableScreen'); context.dispatch('enableScreen');
} }
else { else {
@ -376,25 +372,25 @@ export default {
} }
}, },
join(context, conferenceId) { join(context, conferenceId) {
if(context.getters.hasLocalMediaStream) { if (context.getters.hasLocalMediaStream) {
context.commit('joinRequesting'); context.commit('joinRequesting');
Vue.$conference.joinConference({ Vue.$conference.joinConference({
conferenceName: conferenceId, conferenceName: conferenceId,
displayName: context.getters.username displayName: context.getters.username
}).then(()=>{ }).then(() => {
context.commit('joinSucceeded'); context.commit('joinSucceeded');
}).catch((err)=>{ }).catch((err) => {
context.commit('joinFailed', err.message); context.commit('joinFailed', err.message);
}); });
} }
}, },
leave(context) { leave(context) {
if(context.getters.isJoined) { if (context.getters.isJoined) {
context.commit('leaveRequesting'); context.commit('leaveRequesting');
Vue.$conference.leaveConference().then(()=>{ Vue.$conference.leaveConference().then(() => {
context.commit('leaveSucceeded'); context.commit('leaveSucceeded');
context.commit('disposeLocalMedia'); context.commit('disposeLocalMedia');
}).catch((err)=>{ }).catch((err) => {
context.commit('leaveFailed', err.message); context.commit('leaveFailed', err.message);
}); });
} }

@ -122,18 +122,11 @@ export const store = new Vuex.Store({
}).onConferenceParticipantJoined((participant)=>{ }).onConferenceParticipantJoined((participant)=>{
store.commit('conference/participantJoined', participant); store.commit('conference/participantJoined', participant);
participant.onMediaStream(()=>{ participant.onMediaStream(()=>{
store.commit('conference/removeRemoteMedia', participant.id);
store.commit('conference/addRemoteMedia', participant.id); store.commit('conference/addRemoteMedia', participant.id);
}) }).onMediaEnded(()=>{
participant.onMediaStarted(()=>{
store.commit('conference/addRemoteMedia', participant.id);
})
participant.onMediaRecovered(()=>{
store.commit('conference/addRemoteMedia', participant.id);
})
participant.onMediaEnded(()=>{
store.commit('conference/removeRemoteMedia', participant.id); store.commit('conference/removeRemoteMedia', participant.id);
}) });
}).onConferenceParticipantLeft((participant)=>{ }).onConferenceParticipantLeft((participant)=>{
store.commit('conference/participantLeft', participant); store.commit('conference/participantLeft', participant);
}).onConferenceEvent((event)=>{ }).onConferenceEvent((event)=>{

Loading…
Cancel
Save