add muting again, separate from changing media
This commit is contained in:
parent
ac60d1b61d
commit
cdb2a79b62
5 changed files with 111 additions and 14 deletions
|
@ -57,7 +57,7 @@ export class CallViewModel extends ViewModel<Options> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleVideo() {
|
async toggleVideo() {
|
||||||
//this.call.setMuted();
|
this.call.setMuted(this.call.muteSettings.toggleCamera());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Reference in a new issue