forked from mystiq/hydrogen-web
Merge branch 'bwindels/calls' into thirdroom/dev
This commit is contained in:
commit
bf0638b2f3
6 changed files with 129 additions and 52 deletions
|
@ -67,14 +67,6 @@ export class CallViewModel extends ViewModel<Options> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCameraMuted(): boolean {
|
|
||||||
return this.call.muteSettings.camera;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isMicrophoneMuted(): boolean {
|
|
||||||
return this.call.muteSettings.microphone;
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleVideo() {
|
async toggleVideo() {
|
||||||
this.call.setMuted(this.call.muteSettings.toggleCamera());
|
this.call.setMuted(this.call.muteSettings.toggleCamera());
|
||||||
}
|
}
|
||||||
|
@ -95,11 +87,11 @@ class OwnMemberViewModel extends ViewModel<OwnMemberOptions> implements IStreamV
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCameraMuted(): boolean {
|
get isCameraMuted(): boolean {
|
||||||
return this.call.muteSettings.camera ?? !!getStreamVideoTrack(this.stream);
|
return isMuted(this.call.muteSettings.camera, !!getStreamVideoTrack(this.stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMicrophoneMuted(): boolean {
|
get isMicrophoneMuted(): boolean {
|
||||||
return this.call.muteSettings.microphone ?? !!getStreamAudioTrack(this.stream);
|
return isMuted(this.call.muteSettings.microphone, !!getStreamAudioTrack(this.stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
get avatarLetter(): string {
|
get avatarLetter(): string {
|
||||||
|
@ -135,11 +127,11 @@ export class CallMemberViewModel extends ViewModel<MemberOptions> implements ISt
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCameraMuted(): boolean {
|
get isCameraMuted(): boolean {
|
||||||
return this.member.remoteMuteSettings?.camera ?? !getStreamVideoTrack(this.stream);
|
return isMuted(this.member.remoteMuteSettings?.camera, !!getStreamVideoTrack(this.stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMicrophoneMuted(): boolean {
|
get isMicrophoneMuted(): boolean {
|
||||||
return this.member.remoteMuteSettings?.microphone ?? !getStreamAudioTrack(this.stream);
|
return isMuted(this.member.remoteMuteSettings?.microphone, !!getStreamAudioTrack(this.stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
get avatarLetter(): string {
|
get avatarLetter(): string {
|
||||||
|
@ -178,3 +170,11 @@ export interface IStreamViewModel extends AvatarSource, ViewModel {
|
||||||
get isCameraMuted(): boolean;
|
get isCameraMuted(): boolean;
|
||||||
get isMicrophoneMuted(): boolean;
|
get isMicrophoneMuted(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMuted(muted: boolean | undefined, hasTrack: boolean) {
|
||||||
|
if (muted) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return !hasTrack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -184,6 +184,7 @@ export class PeerCall implements IDisposable {
|
||||||
}
|
}
|
||||||
// after adding the local tracks, and wait for handleNegotiation to be called,
|
// after adding the local tracks, and wait for handleNegotiation to be called,
|
||||||
// or invite glare where we give up our invite and answer instead
|
// or invite glare where we give up our invite and answer instead
|
||||||
|
// TODO: we don't actually use this
|
||||||
await this.waitForState([CallState.InviteSent, CallState.CreateAnswer]);
|
await this.waitForState([CallState.InviteSent, CallState.CreateAnswer]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -902,6 +903,9 @@ export class PeerCall implements IDisposable {
|
||||||
|
|
||||||
|
|
||||||
private onRemoteTrack(track: Track, streams: ReadonlyArray<Stream>, log: ILogItem) {
|
private onRemoteTrack(track: Track, streams: ReadonlyArray<Stream>, log: ILogItem) {
|
||||||
|
log.set("kind", track.kind);
|
||||||
|
log.set("id", track.id);
|
||||||
|
log.set("streams", streams.map(s => s.id));
|
||||||
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});
|
||||||
return;
|
return;
|
||||||
|
@ -932,39 +936,49 @@ export class PeerCall implements IDisposable {
|
||||||
disposeListener,
|
disposeListener,
|
||||||
stream
|
stream
|
||||||
});
|
});
|
||||||
this.updateRemoteMedia(log);
|
|
||||||
}
|
}
|
||||||
|
this.updateRemoteMedia(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateRemoteMedia(log: ILogItem): void {
|
private updateRemoteMedia(log: ILogItem): void {
|
||||||
this._remoteMedia.userMedia = undefined;
|
log.wrap("reevaluating remote media", log => {
|
||||||
this._remoteMedia.screenShare = undefined;
|
this._remoteMedia.userMedia = undefined;
|
||||||
if (this.remoteSDPStreamMetadata) {
|
this._remoteMedia.screenShare = undefined;
|
||||||
for (const streamDetails of this._remoteStreams.values()) {
|
if (this.remoteSDPStreamMetadata) {
|
||||||
const {stream} = streamDetails;
|
for (const streamDetails of this._remoteStreams.values()) {
|
||||||
const metaData = this.remoteSDPStreamMetadata[stream.id];
|
const {stream} = streamDetails;
|
||||||
if (metaData) {
|
const metaData = this.remoteSDPStreamMetadata[stream.id];
|
||||||
if (metaData.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
if (metaData) {
|
||||||
this._remoteMedia.userMedia = stream;
|
if (metaData.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||||
const audioReceiver = this.findReceiverForStream(TrackKind.Audio, stream.id);
|
this._remoteMedia.userMedia = stream;
|
||||||
if (audioReceiver) {
|
const audioReceiver = this.findReceiverForStream(TrackKind.Audio, stream.id);
|
||||||
audioReceiver.track.enabled = !metaData.audio_muted;
|
if (audioReceiver) {
|
||||||
|
audioReceiver.track.enabled = !metaData.audio_muted;
|
||||||
|
}
|
||||||
|
const videoReceiver = this.findReceiverForStream(TrackKind.Video, stream.id);
|
||||||
|
if (videoReceiver) {
|
||||||
|
videoReceiver.track.enabled = !metaData.video_muted;
|
||||||
|
}
|
||||||
|
this._remoteMuteSettings = new MuteSettings(
|
||||||
|
metaData.audio_muted || !audioReceiver?.track,
|
||||||
|
metaData.video_muted || !videoReceiver?.track
|
||||||
|
);
|
||||||
|
log.log({
|
||||||
|
l: "setting userMedia",
|
||||||
|
micMuted: this._remoteMuteSettings.microphone,
|
||||||
|
cameraMuted: this._remoteMuteSettings.camera
|
||||||
|
});
|
||||||
|
} else if (metaData.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
||||||
|
this._remoteMedia.screenShare = stream;
|
||||||
|
log.log("setting screenShare");
|
||||||
}
|
}
|
||||||
const videoReceiver = this.findReceiverForStream(TrackKind.Video, stream.id);
|
} else {
|
||||||
if (videoReceiver) {
|
log.log({l: "no metadata yet for stream, ignoring for now", id: stream.id});
|
||||||
videoReceiver.track.enabled = !metaData.video_muted;
|
|
||||||
}
|
|
||||||
this._remoteMuteSettings = new MuteSettings(
|
|
||||||
metaData.audio_muted || !audioReceiver?.track,
|
|
||||||
metaData.video_muted || !videoReceiver?.track
|
|
||||||
);
|
|
||||||
} else if (metaData.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
|
||||||
this._remoteMedia.screenShare = stream;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
this.options.emitUpdate(this, undefined);
|
||||||
this.options.emitUpdate(this, undefined);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLocalMedia(localMedia: LocalMedia, logItem: ILogItem): Promise<void> {
|
private updateLocalMedia(localMedia: LocalMedia, logItem: ILogItem): Promise<void> {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
- implement muting tracks with m.call.sdp_stream_metadata_changed
|
- implement muting tracks with m.call.sdp_stream_metadata_changed
|
||||||
- implement renegotiation
|
- implement renegotiation
|
||||||
- making logging better
|
- making logging better
|
||||||
- finish session id support
|
- DONE: finish session id support
|
||||||
- call peers are essentially identified by (userid, deviceid, sessionid). If see a new session id, we first disconnect from the current member so we're ready to connect with a clean slate again (in a member event, also in to_device? no harm I suppose, given olm encryption ensures you can't spoof the deviceid).
|
- call peers are essentially identified by (userid, deviceid, sessionid). If see a new session id, we first disconnect from the current member so we're ready to connect with a clean slate again (in a member event, also in to_device? no harm I suppose, given olm encryption ensures you can't spoof the deviceid).
|
||||||
- implement to_device messages arriving before m.call(.member) state event
|
- implement to_device messages arriving before m.call(.member) state event
|
||||||
- reeable crypto & implement fetching olm keys before sending encrypted signalling message
|
- reeable crypto & implement fetching olm keys before sending encrypted signalling message
|
||||||
|
|
|
@ -65,6 +65,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
private _memberOptions: MemberOptions;
|
private _memberOptions: MemberOptions;
|
||||||
private _state: GroupCallState;
|
private _state: GroupCallState;
|
||||||
private localMuteSettings: MuteSettings = new MuteSettings(false, false);
|
private localMuteSettings: MuteSettings = new MuteSettings(false, false);
|
||||||
|
private bufferedDeviceMessages = new Map<string, Set<SignallingMessage<MGroupCallBase>>>();
|
||||||
|
|
||||||
private _deviceIndex?: number;
|
private _deviceIndex?: number;
|
||||||
private _eventTimestamp?: number;
|
private _eventTimestamp?: number;
|
||||||
|
@ -235,7 +236,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
const device = devices[deviceIndex];
|
const device = devices[deviceIndex];
|
||||||
const deviceId = device.device_id;
|
const deviceId = device.device_id;
|
||||||
const memberKey = getMemberKey(userId, deviceId);
|
const memberKey = getMemberKey(userId, deviceId);
|
||||||
log.wrap({l: "update device member", id: memberKey}, log => {
|
log.wrap({l: "update device member", id: memberKey, sessionId: device.session_id}, log => {
|
||||||
if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) {
|
if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) {
|
||||||
|
|
||||||
this._deviceIndex = deviceIndex;
|
this._deviceIndex = deviceIndex;
|
||||||
|
@ -250,7 +251,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
let member = this._members.get(memberKey);
|
let member = this._members.get(memberKey);
|
||||||
if (member) {
|
if (member) {
|
||||||
log.set("update", true);
|
log.set("update", true);
|
||||||
member!.updateCallInfo(device, deviceIndex, eventTimestamp);
|
member!.updateCallInfo(device, deviceIndex, eventTimestamp, log);
|
||||||
} else {
|
} else {
|
||||||
const logItem = this.logItem.child({l: "member", id: memberKey});
|
const logItem = this.logItem.child({l: "member", id: memberKey});
|
||||||
log.set("add", true);
|
log.set("add", true);
|
||||||
|
@ -265,6 +266,9 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
member.connect(this._localMedia!.clone(), this.localMuteSettings);
|
member.connect(this._localMedia!.clone(), this.localMuteSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// flush pending messages, either after having created the member,
|
||||||
|
// or updated the session id with updateCallInfo
|
||||||
|
this.flushPendingDeviceMessages(member, log);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -298,6 +302,23 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private flushPendingDeviceMessages(member: Member, log: ILogItem) {
|
||||||
|
const memberKey = getMemberKey(member.userId, member.deviceId);
|
||||||
|
const bufferedMessages = this.bufferedDeviceMessages.get(memberKey);
|
||||||
|
// check if we have any pending message for the member with (userid, deviceid, sessionid)
|
||||||
|
if (bufferedMessages) {
|
||||||
|
for (const message of bufferedMessages) {
|
||||||
|
if (message.content.sender_session_id === member.sessionId) {
|
||||||
|
member.handleDeviceMessage(message, log);
|
||||||
|
bufferedMessages.delete(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bufferedMessages.size === 0) {
|
||||||
|
this.bufferedDeviceMessages.delete(memberKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getDeviceIdsForUserId(userId: string): string[] {
|
private getDeviceIdsForUserId(userId: string): string[] {
|
||||||
return Array.from(this._members.keys())
|
return Array.from(this._members.keys())
|
||||||
.filter(key => memberKeyIsForUser(key, userId))
|
.filter(key => memberKeyIsForUser(key, userId))
|
||||||
|
@ -338,13 +359,27 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, userId: string, deviceId: string, syncLog: ILogItem) {
|
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, userId: string, deviceId: string, syncLog: ILogItem) {
|
||||||
// TODO: return if we are not membering to the call
|
// TODO: return if we are not membering to the call
|
||||||
let member = this._members.get(getMemberKey(userId, deviceId));
|
const key = getMemberKey(userId, deviceId);
|
||||||
if (member) {
|
let member = this._members.get(key);
|
||||||
member.handleDeviceMessage(message, deviceId, syncLog);
|
if (member && message.content.sender_session_id === member.sessionId) {
|
||||||
|
member.handleDeviceMessage(message, syncLog);
|
||||||
} else {
|
} else {
|
||||||
const item = this.logItem.log({l: "could not find member for signalling message", userId, deviceId});
|
const item = this.logItem.log({
|
||||||
|
l: "buffering to_device message, member not found",
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
sessionId: message.content.sender_session_id,
|
||||||
|
type: message.type
|
||||||
|
});
|
||||||
syncLog.refDetached(item);
|
syncLog.refDetached(item);
|
||||||
// we haven't received the m.call.member yet for this caller. buffer the device messages or create the member/call anyway?
|
// we haven't received the m.call.member yet for this caller (or with this session id).
|
||||||
|
// buffer the device messages or create the member/call as it should arrive in a moment
|
||||||
|
let messages = this.bufferedDeviceMessages.get(key);
|
||||||
|
if (!messages) {
|
||||||
|
messages = new Set();
|
||||||
|
this.bufferedDeviceMessages.set(key, messages);
|
||||||
|
}
|
||||||
|
messages.add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ export type Options = Omit<PeerCallOptions, "emitUpdate" | "sendSignallingMessag
|
||||||
confId: string,
|
confId: string,
|
||||||
ownUserId: string,
|
ownUserId: string,
|
||||||
ownDeviceId: string,
|
ownDeviceId: string,
|
||||||
|
// local session id of our client
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
hsApi: HomeServerApi,
|
hsApi: HomeServerApi,
|
||||||
encryptDeviceMessage: (userId: string, message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
|
encryptDeviceMessage: (userId: string, message: SignallingMessage<MGroupCallBase>, log: ILogItem) => Promise<EncryptedMessage>,
|
||||||
|
@ -83,6 +84,11 @@ export class Member {
|
||||||
return this.callDeviceMembership.device_id;
|
return this.callDeviceMembership.device_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** session id of the member */
|
||||||
|
get sessionId(): string {
|
||||||
|
return this.callDeviceMembership.session_id;
|
||||||
|
}
|
||||||
|
|
||||||
get dataChannel(): any | undefined {
|
get dataChannel(): any | undefined {
|
||||||
return this.peerCall?.dataChannel;
|
return this.peerCall?.dataChannel;
|
||||||
}
|
}
|
||||||
|
@ -127,14 +133,35 @@ export class Member {
|
||||||
this.peerCall = undefined;
|
this.peerCall = undefined;
|
||||||
this.localMedia?.dispose();
|
this.localMedia?.dispose();
|
||||||
this.localMedia = undefined;
|
this.localMedia = undefined;
|
||||||
|
this.retryCount = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
updateCallInfo(callDeviceMembership: CallDeviceMembership, deviceIndex: number, eventTimestamp: number) {
|
updateCallInfo(callDeviceMembership: CallDeviceMembership, deviceIndex: number, eventTimestamp: number, log: ILogItem) {
|
||||||
this.callDeviceMembership = callDeviceMembership;
|
log.wrap({l: "updateing device membership", deviceId: this.deviceId}, log => {
|
||||||
this._deviceIndex = deviceIndex;
|
// session id is changing, disconnect so we start with a new slate for the new session
|
||||||
this._eventTimestamp = eventTimestamp;
|
if (callDeviceMembership.session_id !== this.sessionId) {
|
||||||
|
log.wrap({
|
||||||
|
l: "member event changes session id",
|
||||||
|
oldSessionId: this.sessionId,
|
||||||
|
newSessionId: callDeviceMembership.session_id
|
||||||
|
}, log => {
|
||||||
|
// prevent localMedia from being stopped
|
||||||
|
// as connect won't be called again when reconnecting
|
||||||
|
// to the new session
|
||||||
|
const localMedia = this.localMedia;
|
||||||
|
this.localMedia = undefined;
|
||||||
|
this.disconnect(false);
|
||||||
|
// connect again, as the other side might be waiting for our invite
|
||||||
|
// after refreshing
|
||||||
|
this.connect(localMedia!, this.localMuteSettings!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.callDeviceMembership = callDeviceMembership;
|
||||||
|
this._deviceIndex = deviceIndex;
|
||||||
|
this._eventTimestamp = eventTimestamp;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -163,7 +190,7 @@ export class Member {
|
||||||
groupMessage.content.device_id = this.options.ownDeviceId;
|
groupMessage.content.device_id = this.options.ownDeviceId;
|
||||||
groupMessage.content.party_id = this.options.ownDeviceId;
|
groupMessage.content.party_id = this.options.ownDeviceId;
|
||||||
groupMessage.content.sender_session_id = this.options.sessionId;
|
groupMessage.content.sender_session_id = this.options.sessionId;
|
||||||
groupMessage.content.dest_session_id = this.callDeviceMembership.session_id;
|
groupMessage.content.dest_session_id = this.sessionId;
|
||||||
// const encryptedMessages = await this.options.encryptDeviceMessage(this.member.userId, groupMessage, log);
|
// const encryptedMessages = await this.options.encryptDeviceMessage(this.member.userId, groupMessage, log);
|
||||||
// const payload = formatToDeviceMessagesPayload(encryptedMessages);
|
// const payload = formatToDeviceMessagesPayload(encryptedMessages);
|
||||||
const payload = {
|
const payload = {
|
||||||
|
@ -186,7 +213,7 @@ export class Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, deviceId: string, syncLog: ILogItem) {
|
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, syncLog: ILogItem): void {
|
||||||
syncLog.refDetached(this.logItem);
|
syncLog.refDetached(this.logItem);
|
||||||
const destSessionId = message.content.dest_session_id;
|
const destSessionId = message.content.dest_session_id;
|
||||||
if (destSessionId !== this.options.sessionId) {
|
if (destSessionId !== this.options.sessionId) {
|
||||||
|
@ -197,7 +224,7 @@ export class Member {
|
||||||
this.peerCall = this._createPeerCall(message.content.call_id);
|
this.peerCall = this._createPeerCall(message.content.call_id);
|
||||||
}
|
}
|
||||||
if (this.peerCall) {
|
if (this.peerCall) {
|
||||||
this.peerCall.handleIncomingSignallingMessage(message, deviceId);
|
this.peerCall.handleIncomingSignallingMessage(message, this.deviceId);
|
||||||
} else {
|
} else {
|
||||||
// TODO: need to buffer events until invite comes?
|
// TODO: need to buffer events until invite comes?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1197,6 +1197,7 @@ button.RoomDetailsView_row::after {
|
||||||
.StreamView_muteStatus {
|
.StreamView_muteStatus {
|
||||||
align-self: end;
|
align-self: end;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
|
color: var(--text-color--lighter-80);
|
||||||
}
|
}
|
||||||
|
|
||||||
.StreamView_muteStatus.microphoneMuted::before {
|
.StreamView_muteStatus.microphoneMuted::before {
|
||||||
|
|
Loading…
Reference in a new issue