allow unmuting when we don't yet have a mic/cam track
This commit is contained in:
parent
10caba6872
commit
41288683fc
7 changed files with 105 additions and 46 deletions
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,11 +182,19 @@ 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;
|
||||
await Promise.all(Array.from(this._members.values()).map(m => {
|
||||
return m.setMuted(joinedData.localMuteSettings);
|
||||
}));
|
||||
this.emitChange();
|
||||
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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue