allow unmuting when we don't yet have a mic/cam track

This commit is contained in:
Bruno Windels 2022-06-10 17:10:23 +02:00
parent 10caba6872
commit 41288683fc
7 changed files with 105 additions and 46 deletions

View file

@ -26,7 +26,13 @@ const idToPrepend = "icon-url";
function findAndReplaceUrl(decl) {
const value = decl.value;
const parsed = valueParser(value);
let parsed;
try {
parsed = valueParser(value);
} catch (err) {
console.log(`Error trying to parse ${decl}`);
throw err;
}
parsed.walk(node => {
if (node.type !== "function" || node.value !== "url") {
return;

View file

@ -60,18 +60,17 @@ export class CallViewModel extends ViewModel<Options> {
}
get isCameraMuted(): boolean {
return isLocalCameraMuted(this.call);
return this.call.muteSettings?.camera ?? true;
}
get isMicrophoneMuted(): boolean {
return isLocalMicrophoneMuted(this.call);
return this.call.muteSettings?.microphone ?? true;
}
get memberCount(): number {
return this.memberViewModels.length;
}
get name(): string {
return this.call.name;
}
@ -84,22 +83,36 @@ export class CallViewModel extends ViewModel<Options> {
return this.getOption("call");
}
hangup() {
async hangup() {
if (this.call.hasJoined) {
this.call.leave();
await this.call.leave();
}
}
async toggleCamera() {
if (this.call.muteSettings) {
this.call.setMuted(this.call.muteSettings.toggleCamera());
const {localMedia, muteSettings} = this.call;
if (muteSettings && localMedia) {
// unmute but no track?
if (muteSettings.camera && !getStreamVideoTrack(localMedia.userMedia)) {
const stream = await this.platform.mediaDevices.getMediaTracks(!muteSettings.microphone, true);
await this.call.setMedia(localMedia.withUserMedia(stream));
} else {
await this.call.setMuted(muteSettings.toggleCamera());
}
this.emitChange();
}
}
async toggleMicrophone() {
if (this.call.muteSettings) {
this.call.setMuted(this.call.muteSettings.toggleMicrophone());
const {localMedia, muteSettings} = this.call;
if (muteSettings && localMedia) {
// unmute but no track?
if (muteSettings.microphone && !getStreamAudioTrack(localMedia.userMedia)) {
const stream = await this.platform.mediaDevices.getMediaTracks(true, !muteSettings.camera);
await this.call.setMedia(localMedia.withUserMedia(stream));
} else {
await this.call.setMuted(muteSettings.toggleMicrophone());
}
this.emitChange();
}
}
@ -130,11 +143,11 @@ class OwnMemberViewModel extends ViewModel<Options> implements IStreamViewModel
}
get isCameraMuted(): boolean {
return isLocalCameraMuted(this.call);
return this.call.muteSettings?.camera ?? true;
}
get isMicrophoneMuted(): boolean {
return isLocalMicrophoneMuted(this.call);
return this.call.muteSettings?.microphone ?? true;
}
get avatarLetter(): string {
@ -186,11 +199,11 @@ export class CallMemberViewModel extends ViewModel<MemberOptions> implements ISt
}
get isCameraMuted(): boolean {
return isMuted(this.member.remoteMuteSettings?.camera, !!getStreamVideoTrack(this.stream));
return this.member.remoteMuteSettings?.camera ?? true;
}
get isMicrophoneMuted(): boolean {
return isMuted(this.member.remoteMuteSettings?.microphone, !!getStreamAudioTrack(this.stream));
return this.member.remoteMuteSettings?.microphone ?? true;
}
get avatarLetter(): string {
@ -229,19 +242,3 @@ export interface IStreamViewModel extends AvatarSource, ViewModel {
get isCameraMuted(): boolean;
get isMicrophoneMuted(): boolean;
}
function isMuted(muted: boolean | undefined, hasTrack: boolean) {
if (muted) {
return true;
} else {
return !hasTrack;
}
}
function isLocalCameraMuted(call: GroupCall): boolean {
return isMuted(call.muteSettings?.camera, !!getStreamVideoTrack(call.localMedia?.userMedia));
}
function isLocalMicrophoneMuted(call: GroupCall): boolean {
return isMuted(call.muteSettings?.microphone, !!getStreamAudioTrack(call.localMedia?.userMedia));
}

View file

@ -887,8 +887,8 @@ export class PeerCall implements IDisposable {
const streamId = this.localMedia.userMedia.id;
metadata[streamId] = {
purpose: SDPStreamMetadataPurpose.Usermedia,
audio_muted: this.localMuteSettings?.microphone || !getStreamAudioTrack(this.localMedia.userMedia),
video_muted: this.localMuteSettings?.camera || !getStreamVideoTrack(this.localMedia.userMedia),
audio_muted: this.localMuteSettings?.microphone ?? false,
video_muted: this.localMuteSettings?.camera ?? false,
};
}
if (this.localMedia?.screenShare) {
@ -936,7 +936,7 @@ export class PeerCall implements IDisposable {
this.updateRemoteMedia(log);
}
}
})
});
};
stream.addEventListener("removetrack", listener);
const disposeListener = () => {
@ -971,8 +971,10 @@ export class PeerCall implements IDisposable {
videoReceiver.track.enabled = !metaData.video_muted;
}
this._remoteMuteSettings = new MuteSettings(
metaData.audio_muted || !audioReceiver?.track,
metaData.video_muted || !videoReceiver?.track
metaData.audio_muted ?? false,
metaData.video_muted ?? false,
!!audioReceiver?.track ?? false,
!!videoReceiver?.track ?? false
);
log.log({
l: "setting userMedia",

View file

@ -25,14 +25,36 @@ export function getStreamVideoTrack(stream: Stream | undefined): Track | undefin
}
export class MuteSettings {
constructor (public readonly microphone: boolean = false, public readonly camera: boolean = false) {}
constructor (
private readonly isMicrophoneMuted: boolean = false,
private readonly isCameraMuted: boolean = false,
private hasMicrophoneTrack: boolean = false,
private hasCameraTrack: boolean = false,
) {}
updateTrackInfo(userMedia: Stream | undefined) {
this.hasMicrophoneTrack = !!getStreamAudioTrack(userMedia);
this.hasCameraTrack = !!getStreamVideoTrack(userMedia);
}
get microphone(): boolean {
return !this.hasMicrophoneTrack || this.isMicrophoneMuted;
}
get camera(): boolean {
return !this.hasCameraTrack || this.isCameraMuted;
}
toggleCamera(): MuteSettings {
return new MuteSettings(this.microphone, !this.camera);
return new MuteSettings(this.microphone, !this.camera, this.hasMicrophoneTrack, this.hasCameraTrack);
}
toggleMicrophone(): MuteSettings {
return new MuteSettings(!this.microphone, this.camera);
return new MuteSettings(!this.microphone, this.camera, this.hasMicrophoneTrack, this.hasCameraTrack);
}
equals(other: MuteSettings) {
return this.microphone === other.microphone && this.camera === other.camera;
}
}

View file

@ -137,11 +137,13 @@ export class GroupCall extends EventEmitter<{change: never}> {
ownSessionId: this.options.sessionId
});
const membersLogItem = logItem.child("member connections");
const localMuteSettings = new MuteSettings();
localMuteSettings.updateTrackInfo(localMedia.userMedia);
const joinedData = new JoinedData(
logItem,
membersLogItem,
localMedia,
new MuteSettings()
localMuteSettings
);
this.joinedData = joinedData;
await joinedData.logItem.wrap("join", async log => {
@ -163,9 +165,14 @@ export class GroupCall extends EventEmitter<{change: never}> {
if ((this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) && this.joinedData) {
const oldMedia = this.joinedData.localMedia;
this.joinedData.localMedia = localMedia;
// reflect the fact we gained or lost local tracks in the local mute settings
// and update the track info so PeerCall can use it to send up to date metadata,
this.joinedData.localMuteSettings.updateTrackInfo(localMedia.userMedia);
this.emitChange(); //allow listeners to see new media/mute settings
await Promise.all(Array.from(this._members.values()).map(m => {
return m.setMedia(localMedia, oldMedia);
}));
oldMedia?.stopExcept(localMedia);
}
}
@ -175,12 +182,20 @@ export class GroupCall extends EventEmitter<{change: never}> {
if (!joinedData) {
return;
}
const prevMuteSettings = joinedData.localMuteSettings;
// we still update the mute settings if nothing changed because
// you might be muted because you don't have a track or because
// you actively chosen to mute
// (which we want to respect in the future when you add a track)
joinedData.localMuteSettings = muteSettings;
joinedData.localMuteSettings.updateTrackInfo(joinedData.localMedia.userMedia);
if (!prevMuteSettings.equals(muteSettings)) {
await Promise.all(Array.from(this._members.values()).map(m => {
return m.setMuted(joinedData.localMuteSettings);
}));
this.emitChange();
}
}
get muteSettings(): MuteSettings | undefined {
return this.joinedData?.localMuteSettings;

View file

@ -40,6 +40,7 @@ limitations under the License.
border-radius: 8px;
overflow: hidden;
background-color: black;
display: block;
}
.StreamView > * {
@ -98,11 +99,19 @@ limitations under the License.
background-repeat: no-repeat;
}
.CallView_buttons button:disabled {
background-color: var(--accent-color--lighter-10);
}
.CallView_buttons .CallView_hangup {
background-color: var(--error-color);
background-image: url("./icons/hangup.svg?primary=background-color-primary");
}
.CallView_buttons .CallView_hangup:disabled {
background-color: var(--error-color--lighter-10);
}
.CallView_buttons .CallView_mutedMicrophone {
background-color: var(--background-color-primary);
background-image: url("./icons/mic-muted.svg?primary=text-color");

View file

@ -37,12 +37,12 @@ export class CallView extends TemplateView<CallViewModel> {
t.button({className: {
"CallView_mutedMicrophone": vm => vm.isMicrophoneMuted,
"CallView_unmutedMicrophone": vm => !vm.isMicrophoneMuted,
}, onClick: () => vm.toggleMicrophone()}),
}, onClick: disableTargetCallback(() => vm.toggleMicrophone())}),
t.button({className: {
"CallView_mutedCamera": vm => vm.isCameraMuted,
"CallView_unmutedCamera": vm => !vm.isCameraMuted,
}, onClick: () => vm.toggleCamera()}),
t.button({className: "CallView_hangup", onClick: () => vm.hangup()}),
}, onClick: disableTargetCallback(() => vm.toggleCamera())}),
t.button({className: "CallView_hangup", onClick: disableTargetCallback(() => vm.hangup())}),
])
]);
}
@ -122,3 +122,11 @@ class StreamView extends TemplateView<IStreamViewModel> {
this.updateSubViews(value, props);
}
}
function disableTargetCallback(callback: (evt: Event) => Promise<void>): (evt: Event) => Promise<void> {
return async (evt: Event) => {
(evt.target as HTMLElement)?.setAttribute("disabled", "disabled");
await callback(evt);
(evt.target as HTMLElement)?.removeAttribute("disabled");
}
}