add muting again, separate from changing media

This commit is contained in:
Bruno Windels 2022-04-22 14:48:14 +01:00
parent ac60d1b61d
commit cdb2a79b62
5 changed files with 111 additions and 14 deletions

View file

@ -57,7 +57,7 @@ export class CallViewModel extends ViewModel<Options> {
} }
async toggleVideo() { async toggleVideo() {
//this.call.setMuted(); this.call.setMuted(this.call.muteSettings.toggleCamera());
} }
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
import {ObservableMap} from "../../observable/map/ObservableMap"; import {ObservableMap} from "../../observable/map/ObservableMap";
import {recursivelyAssign} from "../../utils/recursivelyAssign"; import {recursivelyAssign} from "../../utils/recursivelyAssign";
import {Disposables, Disposable, IDisposable} from "../../utils/Disposables"; import {Disposables, Disposable, IDisposable} from "../../utils/Disposables";
import {WebRTC, PeerConnection, Receiver, PeerConnectionEventMap} from "../../platform/types/WebRTC"; import {WebRTC, PeerConnection, Transceiver, TransceiverDirection, Sender, Receiver, PeerConnectionEventMap} from "../../platform/types/WebRTC";
import {MediaDevices, Track, TrackKind, Stream, StreamTrackEvent} from "../../platform/types/MediaDevices"; import {MediaDevices, Track, TrackKind, Stream, StreamTrackEvent} from "../../platform/types/MediaDevices";
import {getStreamVideoTrack, getStreamAudioTrack, MuteSettings} from "./common"; import {getStreamVideoTrack, getStreamAudioTrack, MuteSettings} from "./common";
import { import {
@ -166,13 +166,14 @@ export class PeerCall implements IDisposable {
return this._remoteMedia; return this._remoteMedia;
} }
call(localMedia: LocalMedia): Promise<void> { call(localMedia: LocalMedia, localMuteSettings: MuteSettings): Promise<void> {
return this.logItem.wrap("call", async log => { return this.logItem.wrap("call", async log => {
if (this._state !== CallState.Fledgling) { if (this._state !== CallState.Fledgling) {
return; return;
} }
this.direction = CallDirection.Outbound; this.direction = CallDirection.Outbound;
this.setState(CallState.CreateOffer, log); this.setState(CallState.CreateOffer, log);
this.localMuteSettings = localMuteSettings;
await this.updateLocalMedia(localMedia, log); await this.updateLocalMedia(localMedia, log);
if (this.localMedia?.dataChannelOptions) { if (this.localMedia?.dataChannelOptions) {
this._dataChannel = this.peerConnection.createDataChannel("channel", this.localMedia.dataChannelOptions); this._dataChannel = this.peerConnection.createDataChannel("channel", this.localMedia.dataChannelOptions);
@ -183,12 +184,13 @@ export class PeerCall implements IDisposable {
}); });
} }
answer(localMedia: LocalMedia): Promise<void> { answer(localMedia: LocalMedia, localMuteSettings: MuteSettings): Promise<void> {
return this.logItem.wrap("answer", async log => { return this.logItem.wrap("answer", async log => {
if (this._state !== CallState.Ringing) { if (this._state !== CallState.Ringing) {
return; return;
} }
this.setState(CallState.CreateAnswer, log); this.setState(CallState.CreateAnswer, log);
this.localMuteSettings = localMuteSettings;
await this.updateLocalMedia(localMedia, log); await this.updateLocalMedia(localMedia, log);
let myAnswer: RTCSessionDescriptionInit; let myAnswer: RTCSessionDescriptionInit;
try { try {
@ -235,12 +237,54 @@ export class PeerCall implements IDisposable {
}); });
} }
setMuted(localMuteSettings: MuteSettings) {
return this.logItem.wrap("setMuted", async log => {
this.localMuteSettings = localMuteSettings;
log.set("cameraMuted", localMuteSettings.camera);
log.set("microphoneMuted", localMuteSettings.microphone);
if (this.localMedia) {
const userMediaAudio = getStreamAudioTrack(this.localMedia.userMedia);
if (userMediaAudio) {
this.muteTrack(userMediaAudio, this.localMuteSettings.microphone, log);
}
const userMediaVideo = getStreamVideoTrack(this.localMedia.userMedia);
if (userMediaVideo) {
this.muteTrack(userMediaVideo, this.localMuteSettings.camera, log);
}
const content: MCallSDPStreamMetadataChanged<MCallBase> = {
call_id: this.callId,
version: 1,
seq: this.seq++,
[SDPStreamMetadataKey]: this.getSDPMetadata()
};
await this.sendSignallingMessage({type: EventType.SDPStreamMetadataChangedPrefix, content}, log);
}
});
}
hangup(errorCode: CallErrorCode): Promise<void> { hangup(errorCode: CallErrorCode): Promise<void> {
return this.logItem.wrap("hangup", log => { return this.logItem.wrap("hangup", log => {
return this._hangup(errorCode, log); return this._hangup(errorCode, log);
}); });
} }
private muteTrack(track: Track, muted: boolean, log: ILogItem): void {
log.wrap({l: "track", kind: track.kind, id: track.id}, log => {
const enabled = !muted;
log.set("enabled", enabled);
const transceiver = this.findTransceiverForTrack(track);
if (transceiver) {
if (transceiver.sender.track) {
transceiver.sender.track.enabled = enabled;
}
log.set("fromDirection", transceiver.direction);
enableSenderOnTransceiver(transceiver, enabled);
log.set("toDirection", transceiver.direction);
}
});
}
private async _hangup(errorCode: CallErrorCode, log: ILogItem): Promise<void> { private async _hangup(errorCode: CallErrorCode, log: ILogItem): Promise<void> {
if (this._state === CallState.Ended) { if (this._state === CallState.Ended) {
return; return;
@ -872,10 +916,17 @@ export class PeerCall implements IDisposable {
private findReceiverForStream(kind: TrackKind, streamId: string): Receiver | undefined { private findReceiverForStream(kind: TrackKind, streamId: string): Receiver | undefined {
return this.peerConnection.getReceivers().find(r => { return this.peerConnection.getReceivers().find(r => {
return r.track.kind === "audio" && this._remoteTrackToStreamId.get(r.track.id) === streamId; return r.track.kind === kind && this._remoteTrackToStreamId.get(r.track.id) === streamId;
}); });
} }
private findTransceiverForTrack(track: Track): Transceiver | undefined {
return this.peerConnection.getTransceivers().find(t => {
return t.sender.track?.id === track.id;
});
}
private onRemoteTrack(track: Track, streams: ReadonlyArray<Stream>, log: ILogItem) { private onRemoteTrack(track: Track, streams: ReadonlyArray<Stream>, log: ILogItem) {
if (streams.length === 0) { if (streams.length === 0) {
log.log({l: `ignoring ${track.kind} streamless track`, id: track.id}); log.log({l: `ignoring ${track.kind} streamless track`, id: track.id});
@ -1072,7 +1123,22 @@ export function handlesEventType(eventType: string): boolean {
eventType === EventType.Negotiate; eventType === EventType.Negotiate;
} }
function enableSenderOnTransceiver(transceiver: Transceiver, enabled: boolean) {
export function tests() { return enableTransceiver(transceiver, enabled, "sendonly", "recvonly");
}
function enableTransceiver(transceiver: Transceiver, enabled: boolean, exclusiveValue: TransceiverDirection, excludedValue: TransceiverDirection) {
if (enabled) {
if (transceiver.direction === "inactive") {
transceiver.direction = exclusiveValue;
} else {
transceiver.direction = "sendrecv";
}
} else {
if (transceiver.direction === "sendrecv") {
transceiver.direction = excludedValue;
} else {
transceiver.direction = "inactive";
}
}
} }

View file

@ -26,4 +26,12 @@ export function getStreamVideoTrack(stream: Stream | undefined): Track | undefin
export class MuteSettings { export class MuteSettings {
constructor (public readonly microphone: boolean, public readonly camera: boolean) {} constructor (public readonly microphone: boolean, public readonly camera: boolean) {}
toggleCamera(): MuteSettings {
return new MuteSettings(this.microphone, !this.camera);
}
toggleMicrophone(): MuteSettings {
return new MuteSettings(!this.microphone, this.camera);
}
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import {ObservableMap} from "../../../observable/map/ObservableMap"; import {ObservableMap} from "../../../observable/map/ObservableMap";
import {Member} from "./Member"; import {Member} from "./Member";
import {LocalMedia} from "../LocalMedia"; import {LocalMedia} from "../LocalMedia";
import {MuteSettings} from "../common";
import {RoomMember} from "../../room/members/RoomMember"; import {RoomMember} from "../../room/members/RoomMember";
import {EventEmitter} from "../../../utils/EventEmitter"; import {EventEmitter} from "../../../utils/EventEmitter";
import {EventType, CallIntent} from "../callEventTypes"; import {EventType, CallIntent} from "../callEventTypes";
@ -63,6 +64,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
private _localMedia?: LocalMedia = undefined; private _localMedia?: LocalMedia = undefined;
private _memberOptions: MemberOptions; private _memberOptions: MemberOptions;
private _state: GroupCallState; private _state: GroupCallState;
private localMuteSettings: MuteSettings = new MuteSettings(false, false);
constructor( constructor(
public readonly id: string, public readonly id: string,
@ -118,7 +120,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
this.emitChange(); this.emitChange();
// send invite to all members that are < my userId // send invite to all members that are < my userId
for (const [,member] of this._members) { for (const [,member] of this._members) {
member.connect(this._localMedia!.clone()); member.connect(this._localMedia!.clone(), this.localMuteSettings);
} }
}); });
} }
@ -134,6 +136,19 @@ export class GroupCall extends EventEmitter<{change: never}> {
} }
} }
setMuted(muteSettings: MuteSettings) {
this.localMuteSettings = muteSettings;
if (this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) {
for (const [,member] of this._members) {
member.setMuted(this.localMuteSettings);
}
}
}
get muteSettings(): MuteSettings {
return this.localMuteSettings;
}
get hasJoined() { get hasJoined() {
return this._state === GroupCallState.Joining || this._state === GroupCallState.Joined; return this._state === GroupCallState.Joining || this._state === GroupCallState.Joined;
} }
@ -230,7 +245,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
this._members.add(memberKey, member); this._members.add(memberKey, member);
if (this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) { if (this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) {
// Safari can't send a MediaStream to multiple sources, so clone it // Safari can't send a MediaStream to multiple sources, so clone it
member.connect(this._localMedia!.clone()); member.connect(this._localMedia!.clone(), this.localMuteSettings);
} }
} }
} }

View file

@ -19,6 +19,7 @@ import {makeTxnId, makeId} from "../../common";
import {EventType, CallErrorCode} from "../callEventTypes"; import {EventType, CallErrorCode} from "../callEventTypes";
import {formatToDeviceMessagesPayload} from "../../common"; import {formatToDeviceMessagesPayload} from "../../common";
import type {MuteSettings} from "../common";
import type {Options as PeerCallOptions, RemoteMedia} from "../PeerCall"; import type {Options as PeerCallOptions, RemoteMedia} from "../PeerCall";
import type {LocalMedia} from "../LocalMedia"; import type {LocalMedia} from "../LocalMedia";
import type {HomeServerApi} from "../../net/HomeServerApi"; import type {HomeServerApi} from "../../net/HomeServerApi";
@ -50,6 +51,7 @@ const errorCodesWithoutRetry = [
export class Member { export class Member {
private peerCall?: PeerCall; private peerCall?: PeerCall;
private localMedia?: LocalMedia; private localMedia?: LocalMedia;
private localMuteSettings?: MuteSettings;
private retryCount: number = 0; private retryCount: number = 0;
constructor( constructor(
@ -80,9 +82,10 @@ export class Member {
} }
/** @internal */ /** @internal */
connect(localMedia: LocalMedia) { connect(localMedia: LocalMedia, localMuteSettings: MuteSettings) {
this.logItem.wrap("connect", () => { this.logItem.wrap("connect", () => {
this.localMedia = localMedia; this.localMedia = localMedia;
this.localMuteSettings = localMuteSettings;
// otherwise wait for it to connect // otherwise wait for it to connect
let shouldInitiateCall; let shouldInitiateCall;
// the lexicographically lower side initiates the call // the lexicographically lower side initiates the call
@ -93,7 +96,7 @@ export class Member {
} }
if (shouldInitiateCall) { if (shouldInitiateCall) {
this.peerCall = this._createPeerCall(makeId("c")); this.peerCall = this._createPeerCall(makeId("c"));
this.peerCall.call(localMedia); this.peerCall.call(localMedia, localMuteSettings);
} }
}); });
} }
@ -121,7 +124,7 @@ export class Member {
/** @internal */ /** @internal */
emitUpdate = (peerCall: PeerCall, params: any) => { emitUpdate = (peerCall: PeerCall, params: any) => {
if (peerCall.state === CallState.Ringing) { if (peerCall.state === CallState.Ringing) {
peerCall.answer(this.localMedia!); peerCall.answer(this.localMedia!, this.localMuteSettings!);
} }
else if (peerCall.state === CallState.Ended) { else if (peerCall.state === CallState.Ended) {
const hangupReason = peerCall.hangupReason; const hangupReason = peerCall.hangupReason;
@ -130,7 +133,7 @@ export class Member {
if (hangupReason && !errorCodesWithoutRetry.includes(hangupReason)) { if (hangupReason && !errorCodesWithoutRetry.includes(hangupReason)) {
this.retryCount += 1; this.retryCount += 1;
if (this.retryCount <= 3) { if (this.retryCount <= 3) {
this.connect(this.localMedia!); this.connect(this.localMedia!, this.localMuteSettings!);
} }
} }
} }
@ -190,6 +193,11 @@ export class Member {
await this.peerCall?.setMedia(this.localMedia); await this.peerCall?.setMedia(this.localMedia);
} }
setMuted(muteSettings: MuteSettings) {
this.localMuteSettings = muteSettings;
this.peerCall?.setMuted(muteSettings);
}
private _createPeerCall(callId: string): PeerCall { private _createPeerCall(callId: string): PeerCall {
return new PeerCall(callId, Object.assign({}, this.options, { return new PeerCall(callId, Object.assign({}, this.options, {
emitUpdate: this.emitUpdate, emitUpdate: this.emitUpdate,