forked from mystiq/hydrogen-web
more improvements, make hangup work
This commit is contained in:
parent
0a37fd561e
commit
a0a07355d4
16 changed files with 182 additions and 56 deletions
|
@ -33,16 +33,26 @@ export class CallViewModel extends ViewModel<Options> {
|
||||||
.sortValues((a, b) => a.compare(b));
|
.sortValues((a, b) => a.compare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get call(): GroupCall {
|
||||||
|
return this.getOption("call");
|
||||||
|
}
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this.getOption("call").name;
|
return this.call.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id(): string {
|
get id(): string {
|
||||||
return this.getOption("call").id;
|
return this.call.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get localTracks(): Track[] {
|
get localTracks(): Track[] {
|
||||||
return this.getOption("call").localMedia?.tracks ?? [];
|
return this.call.localMedia?.tracks ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
leave() {
|
||||||
|
if (this.call.hasJoined) {
|
||||||
|
this.call.leave();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,12 +60,16 @@ type MemberOptions = BaseOptions & {member: Member};
|
||||||
|
|
||||||
export class CallMemberViewModel extends ViewModel<MemberOptions> {
|
export class CallMemberViewModel extends ViewModel<MemberOptions> {
|
||||||
get tracks(): Track[] {
|
get tracks(): Track[] {
|
||||||
return this.getOption("member").remoteTracks;
|
return this.member.remoteTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get member(): Member {
|
||||||
|
return this.getOption("member");
|
||||||
}
|
}
|
||||||
|
|
||||||
compare(other: CallMemberViewModel): number {
|
compare(other: CallMemberViewModel): number {
|
||||||
const myUserId = this.getOption("member").member.userId;
|
const myUserId = this.member.member.userId;
|
||||||
const otherUserId = other.getOption("member").member.userId;
|
const otherUserId = other.member.member.userId;
|
||||||
if(myUserId === otherUserId) {
|
if(myUserId === otherUserId) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,12 +49,17 @@ export class RoomViewModel extends ViewModel {
|
||||||
_setupCallViewModel() {
|
_setupCallViewModel() {
|
||||||
// pick call for this room with lowest key
|
// pick call for this room with lowest key
|
||||||
const calls = this.getOption("session").callHandler.calls;
|
const calls = this.getOption("session").callHandler.calls;
|
||||||
this._callObservable = new PickMapObservableValue(calls.filterValues(c => c.roomId === this._room.id && c.hasJoined));
|
this._callObservable = new PickMapObservableValue(calls.filterValues(c => {
|
||||||
|
return c.roomId === this._room.id && c.hasJoined;
|
||||||
|
}));
|
||||||
this._callViewModel = undefined;
|
this._callViewModel = undefined;
|
||||||
this.track(this._callObservable.subscribe(call => {
|
this.track(this._callObservable.subscribe(call => {
|
||||||
|
if (call && this._callViewModel && call.id === this._callViewModel.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._callViewModel = this.disposeTracked(this._callViewModel);
|
this._callViewModel = this.disposeTracked(this._callViewModel);
|
||||||
if (call) {
|
if (call) {
|
||||||
this._callViewModel = new CallViewModel(this.childOptions({call}));
|
this._callViewModel = this.track(new CallViewModel(this.childOptions({call})));
|
||||||
}
|
}
|
||||||
this.emitChange("callViewModel");
|
this.emitChange("callViewModel");
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -49,14 +49,6 @@ export class BaseMessageTile extends SimpleTile {
|
||||||
return `https://matrix.to/#/${encodeURIComponent(this.sender)}`;
|
return `https://matrix.to/#/${encodeURIComponent(this.sender)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get displayName() {
|
|
||||||
return this._entry.displayName || this.sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
get sender() {
|
|
||||||
return this._entry.sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
get memberPanelLink() {
|
get memberPanelLink() {
|
||||||
return `${this.urlCreator.urlUntilSegment("room")}/member/${this.sender}`;
|
return `${this.urlCreator.urlUntilSegment("room")}/member/${this.sender}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,27 @@ import {LocalMedia} from "../../../../../matrix/calls/LocalMedia";
|
||||||
|
|
||||||
export class CallTile extends SimpleTile {
|
export class CallTile extends SimpleTile {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
const calls = this.getOption("session").callHandler.calls;
|
||||||
|
this._call = calls.get(this._entry.stateKey);
|
||||||
|
this._callSubscription = undefined;
|
||||||
|
if (this._call) {
|
||||||
|
this._callSubscription = this._call.disposableOn("change", () => {
|
||||||
|
// unsubscribe when terminated
|
||||||
|
if (this._call.isTerminated) {
|
||||||
|
this._callSubscription = this._callSubscription();
|
||||||
|
this._call = undefined;
|
||||||
|
}
|
||||||
|
this.emitChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get confId() {
|
||||||
|
return this._entry.stateKey;
|
||||||
|
}
|
||||||
|
|
||||||
get shape() {
|
get shape() {
|
||||||
return "call";
|
return "call";
|
||||||
}
|
}
|
||||||
|
@ -32,17 +53,43 @@ export class CallTile extends SimpleTile {
|
||||||
return this._entry.content["m.name"];
|
return this._entry.content["m.name"];
|
||||||
}
|
}
|
||||||
|
|
||||||
get _call() {
|
get canJoin() {
|
||||||
const calls = this.getOption("session").callHandler.calls;
|
return this._call && !this._call.hasJoined;
|
||||||
return calls.get(this._entry.stateKey);
|
}
|
||||||
|
|
||||||
|
get canLeave() {
|
||||||
|
return this._call && this._call.hasJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
if (this._call) {
|
||||||
|
if (this._call.hasJoined) {
|
||||||
|
return `Ongoing call (${this.name}, ${this.confId})`;
|
||||||
|
} else {
|
||||||
|
return `${this.displayName} started a call (${this.name}, ${this.confId})`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `Call finished, started by ${this.displayName} (${this.name}, ${this.confId})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async join() {
|
async join() {
|
||||||
const call = this._call;
|
if (this.canJoin) {
|
||||||
if (call) {
|
|
||||||
const mediaTracks = await this.platform.mediaDevices.getMediaTracks(false, true);
|
const mediaTracks = await this.platform.mediaDevices.getMediaTracks(false, true);
|
||||||
const localMedia = new LocalMedia().withTracks(mediaTracks);
|
const localMedia = new LocalMedia().withTracks(mediaTracks);
|
||||||
await call.join(localMedia);
|
await this._call.join(localMedia);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async leave() {
|
||||||
|
if (this.canLeave) {
|
||||||
|
this._call.leave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this._callSubscription) {
|
||||||
|
this._callSubscription = this._callSubscription();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,4 +154,12 @@ export class SimpleTile extends ViewModel {
|
||||||
get _ownMember() {
|
get _ownMember() {
|
||||||
return this._options.timeline.me;
|
return this._options.timeline.me;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayName() {
|
||||||
|
return this._entry.displayName || this.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sender() {
|
||||||
|
return this._entry.sender;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,9 @@ export function tilesCreator(baseOptions) {
|
||||||
case "m.room.encryption":
|
case "m.room.encryption":
|
||||||
return new EncryptionEnabledTile(options);
|
return new EncryptionEnabledTile(options);
|
||||||
case "m.call":
|
case "m.call":
|
||||||
return entry.stateKey ? new CallTile(options) : null;
|
// if prevContent is present, it's an update to a call event, which we don't render
|
||||||
|
// as the original event is updated through the call object which receive state event updates
|
||||||
|
return entry.stateKey && !entry.prevContent ? new CallTile(options) : null;
|
||||||
default:
|
default:
|
||||||
// unknown type not rendered
|
// unknown type not rendered
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -86,7 +86,6 @@ export class Session {
|
||||||
// although we probably already fetched all devices to send messages in the likely e2ee room
|
// although we probably already fetched all devices to send messages in the likely e2ee room
|
||||||
await this._deviceTracker.trackRoom(this.rooms.get(roomId), log);
|
await this._deviceTracker.trackRoom(this.rooms.get(roomId), log);
|
||||||
const devices = await this._deviceTracker.devicesForRoomMembers(roomId, [userId], this._hsApi, log);
|
const devices = await this._deviceTracker.devicesForRoomMembers(roomId, [userId], this._hsApi, log);
|
||||||
console.log("devices", devices);
|
|
||||||
const encryptedMessage = await this._olmEncryption.encrypt(message.type, message.content, devices, this._hsApi, log);
|
const encryptedMessage = await this._olmEncryption.encrypt(message.type, message.content, devices, this._hsApi, log);
|
||||||
return encryptedMessage;
|
return encryptedMessage;
|
||||||
},
|
},
|
||||||
|
|
|
@ -112,7 +112,7 @@ export class CallHandler {
|
||||||
const callId = event.state_key;
|
const callId = event.state_key;
|
||||||
let call = this._calls.get(callId);
|
let call = this._calls.get(callId);
|
||||||
if (call) {
|
if (call) {
|
||||||
call.updateCallEvent(event);
|
call.updateCallEvent(event.content);
|
||||||
if (call.isTerminated) {
|
if (call.isTerminated) {
|
||||||
this._calls.remove(call.id);
|
this._calls.remove(call.id);
|
||||||
}
|
}
|
||||||
|
@ -125,13 +125,13 @@ export class CallHandler {
|
||||||
private handleCallMemberEvent(event: StateEvent) {
|
private handleCallMemberEvent(event: StateEvent) {
|
||||||
const userId = event.state_key;
|
const userId = event.state_key;
|
||||||
const calls = event.content["m.calls"] ?? [];
|
const calls = event.content["m.calls"] ?? [];
|
||||||
const newCallIdsMemberOf = new Set<string>(calls.map(call => {
|
for (const call of calls) {
|
||||||
const callId = call["m.call_id"];
|
const callId = call["m.call_id"];
|
||||||
const groupCall = this._calls.get(callId);
|
const groupCall = this._calls.get(callId);
|
||||||
// TODO: also check the member when receiving the m.call event
|
// TODO: also check the member when receiving the m.call event
|
||||||
groupCall?.addMember(userId, call);
|
groupCall?.addMember(userId, call);
|
||||||
return callId;
|
};
|
||||||
}));
|
const newCallIdsMemberOf = new Set<string>(calls.map(call => call["m.call_id"]));
|
||||||
let previousCallIdsMemberOf = this.memberToCallIds.get(userId);
|
let previousCallIdsMemberOf = this.memberToCallIds.get(userId);
|
||||||
// remove user as member of any calls not present anymore
|
// remove user as member of any calls not present anymore
|
||||||
if (previousCallIdsMemberOf) {
|
if (previousCallIdsMemberOf) {
|
||||||
|
|
|
@ -60,4 +60,10 @@ export class LocalMedia {
|
||||||
}
|
}
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.cameraTrack?.stop();
|
||||||
|
this.microphoneTrack?.stop();
|
||||||
|
this.screenShareTrack?.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -748,6 +748,10 @@ export class PeerCall implements IDisposable {
|
||||||
this.disposables.dispose();
|
this.disposables.dispose();
|
||||||
this.peerConnection.dispose();
|
this.peerConnection.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.peerConnection.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {Member} from "./Member";
|
||||||
import {LocalMedia} from "../LocalMedia";
|
import {LocalMedia} from "../LocalMedia";
|
||||||
import {RoomMember} from "../../room/members/RoomMember";
|
import {RoomMember} from "../../room/members/RoomMember";
|
||||||
import {makeId} from "../../common";
|
import {makeId} from "../../common";
|
||||||
|
import {EventEmitter} from "../../../utils/EventEmitter";
|
||||||
|
|
||||||
import type {Options as MemberOptions} from "./Member";
|
import type {Options as MemberOptions} from "./Member";
|
||||||
import type {BaseObservableMap} from "../../../observable/map/BaseObservableMap";
|
import type {BaseObservableMap} from "../../../observable/map/BaseObservableMap";
|
||||||
|
@ -31,6 +32,9 @@ import type {EncryptedMessage} from "../../e2ee/olm/Encryption";
|
||||||
import type {ILogItem} from "../../../logging/types";
|
import type {ILogItem} from "../../../logging/types";
|
||||||
import type {Storage} from "../../storage/idb/Storage";
|
import type {Storage} from "../../storage/idb/Storage";
|
||||||
|
|
||||||
|
const CALL_TYPE = "m.call";
|
||||||
|
const CALL_MEMBER_TYPE = "m.call.member";
|
||||||
|
|
||||||
export enum GroupCallState {
|
export enum GroupCallState {
|
||||||
Fledgling = "fledgling",
|
Fledgling = "fledgling",
|
||||||
Creating = "creating",
|
Creating = "creating",
|
||||||
|
@ -46,7 +50,7 @@ export type Options = Omit<MemberOptions, "emitUpdate" | "confId" | "encryptDevi
|
||||||
ownDeviceId: string
|
ownDeviceId: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class GroupCall {
|
export class GroupCall extends EventEmitter<{change: never}> {
|
||||||
public readonly id: string;
|
public readonly id: string;
|
||||||
private readonly _members: ObservableMap<string, Member> = new ObservableMap();
|
private readonly _members: ObservableMap<string, Member> = new ObservableMap();
|
||||||
private _localMedia?: LocalMedia = undefined;
|
private _localMedia?: LocalMedia = undefined;
|
||||||
|
@ -59,6 +63,7 @@ export class GroupCall {
|
||||||
public readonly roomId: string,
|
public readonly roomId: string,
|
||||||
private readonly options: Options
|
private readonly options: Options
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
this.id = id ?? makeId("conf-");
|
this.id = id ?? makeId("conf-");
|
||||||
this._state = id ? GroupCallState.Created : GroupCallState.Fledgling;
|
this._state = id ? GroupCallState.Created : GroupCallState.Fledgling;
|
||||||
this._memberOptions = Object.assign({}, options, {
|
this._memberOptions = Object.assign({}, options, {
|
||||||
|
@ -87,12 +92,12 @@ export class GroupCall {
|
||||||
}
|
}
|
||||||
this._state = GroupCallState.Joining;
|
this._state = GroupCallState.Joining;
|
||||||
this._localMedia = localMedia;
|
this._localMedia = localMedia;
|
||||||
this.options.emitUpdate(this);
|
this.emitChange();
|
||||||
const memberContent = await this._joinCallMemberContent();
|
const memberContent = await this._createJoinPayload();
|
||||||
// send m.call.member state event
|
// send m.call.member state event
|
||||||
const request = this.options.hsApi.sendState(this.roomId, "m.call.member", this.options.ownUserId, memberContent);
|
const request = this.options.hsApi.sendState(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId, memberContent);
|
||||||
await request.response();
|
await request.response();
|
||||||
this.options.emitUpdate(this);
|
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);
|
member.connect(this._localMedia);
|
||||||
|
@ -107,10 +112,24 @@ export class GroupCall {
|
||||||
const memberContent = await this._leaveCallMemberContent();
|
const memberContent = await this._leaveCallMemberContent();
|
||||||
// send m.call.member state event
|
// send m.call.member state event
|
||||||
if (memberContent) {
|
if (memberContent) {
|
||||||
const request = this.options.hsApi.sendState(this.roomId, "m.call.member", this.options.ownUserId, memberContent);
|
const request = this.options.hsApi.sendState(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId, memberContent);
|
||||||
await request.response();
|
await request.response();
|
||||||
|
// our own user isn't included in members, so not in the count
|
||||||
|
if (this._members.size === 0) {
|
||||||
|
this.terminate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminate() {
|
||||||
|
if (this._state === GroupCallState.Fledgling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const request = this.options.hsApi.sendState(this.roomId, CALL_TYPE, this.id, Object.assign({}, this.callContent, {
|
||||||
|
"m.terminated": true
|
||||||
|
}));
|
||||||
|
await request.response();
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async create(localMedia: LocalMedia, name: string) {
|
async create(localMedia: LocalMedia, name: string) {
|
||||||
|
@ -118,14 +137,16 @@ export class GroupCall {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._state = GroupCallState.Creating;
|
this._state = GroupCallState.Creating;
|
||||||
|
this.emitChange();
|
||||||
this.callContent = {
|
this.callContent = {
|
||||||
"m.type": localMedia.cameraTrack ? "m.video" : "m.voice",
|
"m.type": localMedia.cameraTrack ? "m.video" : "m.voice",
|
||||||
"m.name": name,
|
"m.name": name,
|
||||||
"m.intent": "m.ring"
|
"m.intent": "m.ring"
|
||||||
};
|
};
|
||||||
const request = this.options.hsApi.sendState(this.roomId, "m.call", this.id, this.callContent);
|
const request = this.options.hsApi.sendState(this.roomId, CALL_TYPE, this.id, this.callContent);
|
||||||
await request.response();
|
await request.response();
|
||||||
this._state = GroupCallState.Created;
|
this._state = GroupCallState.Created;
|
||||||
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -134,6 +155,7 @@ export class GroupCall {
|
||||||
if (this._state === GroupCallState.Creating) {
|
if (this._state === GroupCallState.Creating) {
|
||||||
this._state = GroupCallState.Created;
|
this._state = GroupCallState.Created;
|
||||||
}
|
}
|
||||||
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -141,6 +163,7 @@ export class GroupCall {
|
||||||
if (userId === this.options.ownUserId) {
|
if (userId === this.options.ownUserId) {
|
||||||
if (this._state === GroupCallState.Joining) {
|
if (this._state === GroupCallState.Joining) {
|
||||||
this._state = GroupCallState.Joined;
|
this._state = GroupCallState.Joined;
|
||||||
|
this.emitChange();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -160,11 +183,21 @@ export class GroupCall {
|
||||||
removeMember(userId) {
|
removeMember(userId) {
|
||||||
if (userId === this.options.ownUserId) {
|
if (userId === this.options.ownUserId) {
|
||||||
if (this._state === GroupCallState.Joined) {
|
if (this._state === GroupCallState.Joined) {
|
||||||
|
this._localMedia?.dispose();
|
||||||
|
this._localMedia = undefined;
|
||||||
|
for (const [,member] of this._members) {
|
||||||
|
member.disconnect();
|
||||||
|
}
|
||||||
this._state = GroupCallState.Created;
|
this._state = GroupCallState.Created;
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
}
|
const member = this._members.get(userId);
|
||||||
|
if (member) {
|
||||||
this._members.remove(userId);
|
this._members.remove(userId);
|
||||||
|
member.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -179,10 +212,10 @@ export class GroupCall {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _joinCallMemberContent() {
|
private async _createJoinPayload() {
|
||||||
const {storage} = this.options;
|
const {storage} = this.options;
|
||||||
const txn = await storage.readTxn([storage.storeNames.roomState]);
|
const txn = await storage.readTxn([storage.storeNames.roomState]);
|
||||||
const stateEvent = await txn.roomState.get(this.roomId, "m.call.member", this.options.ownUserId);
|
const stateEvent = await txn.roomState.get(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId);
|
||||||
const stateContent = stateEvent?.event?.content ?? {
|
const stateContent = stateEvent?.event?.content ?? {
|
||||||
["m.calls"]: []
|
["m.calls"]: []
|
||||||
};
|
};
|
||||||
|
@ -209,9 +242,18 @@ export class GroupCall {
|
||||||
private async _leaveCallMemberContent(): Promise<Record<string, any> | undefined> {
|
private async _leaveCallMemberContent(): Promise<Record<string, any> | undefined> {
|
||||||
const {storage} = this.options;
|
const {storage} = this.options;
|
||||||
const txn = await storage.readTxn([storage.storeNames.roomState]);
|
const txn = await storage.readTxn([storage.storeNames.roomState]);
|
||||||
const stateEvent = await txn.roomState.get(this.roomId, "m.call.member", this.options.ownUserId);
|
const stateEvent = await txn.roomState.get(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId);
|
||||||
const callsInfo = stateEvent?.event?.content?.["m.calls"];
|
if (stateEvent) {
|
||||||
callsInfo?.filter(c => c["m.call_id"] === this.id);
|
const content = stateEvent.event.content;
|
||||||
return stateEvent?.event.content;
|
const callsInfo = content["m.calls"];
|
||||||
|
content["m.calls"] = callsInfo?.filter(c => c["m.call_id"] !== this.id);
|
||||||
|
return content;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected emitChange() {
|
||||||
|
this.emit("change");
|
||||||
|
this.options.emitUpdate(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,14 @@ export class Member {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
disconnect() {
|
||||||
|
this.peerCall?.close();
|
||||||
|
this.peerCall?.dispose();
|
||||||
|
this.peerCall = undefined;
|
||||||
|
this.localMedia = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
updateCallInfo(memberCallInfo) {
|
updateCallInfo(memberCallInfo) {
|
||||||
// m.calls object from the m.call.member event
|
// m.calls object from the m.call.member event
|
||||||
|
|
|
@ -451,7 +451,7 @@ export class Room extends BaseRoom {
|
||||||
_updateCallHandler(roomResponse, log) {
|
_updateCallHandler(roomResponse, log) {
|
||||||
if (this._callHandler) {
|
if (this._callHandler) {
|
||||||
const stateEvents = roomResponse.state?.events;
|
const stateEvents = roomResponse.state?.events;
|
||||||
if (stateEvents) {
|
if (stateEvents?.length) {
|
||||||
this._callHandler.handleRoomState(this, stateEvents, log);
|
this._callHandler.handleRoomState(this, stateEvents, log);
|
||||||
}
|
}
|
||||||
let timelineEvents = roomResponse.timeline?.events;
|
let timelineEvents = roomResponse.timeline?.events;
|
||||||
|
|
|
@ -60,15 +60,13 @@ export class PickMapObservableValue<K, V> extends BaseObservableValue<V | undefi
|
||||||
onRemove(key: K, value: V): void {
|
onRemove(key: K, value: V): void {
|
||||||
if (key === this.key) {
|
if (key === this.key) {
|
||||||
this.key = undefined;
|
this.key = undefined;
|
||||||
let changed = false;
|
// try to see if there is another key that fullfills pickKey
|
||||||
for (const [key] of this.map) {
|
for (const [key] of this.map) {
|
||||||
changed = this.updateKey(key) || changed;
|
this.updateKey(key) || changed;
|
||||||
}
|
}
|
||||||
if (changed) {
|
|
||||||
this.emit(this.get());
|
this.emit(this.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onSubscribeFirst(): void {
|
onSubscribeFirst(): void {
|
||||||
this.mapSubscription = this.map.subscribe(this);
|
this.mapSubscription = this.map.subscribe(this);
|
||||||
|
|
|
@ -22,7 +22,6 @@ import type {CallViewModel, CallMemberViewModel} from "../../../../../domain/ses
|
||||||
|
|
||||||
function bindVideoTracks<T>(t: TemplateBuilder<T>, video: HTMLVideoElement, propSelector: (vm: T) => Track[]) {
|
function bindVideoTracks<T>(t: TemplateBuilder<T>, video: HTMLVideoElement, propSelector: (vm: T) => Track[]) {
|
||||||
t.mapSideEffect(propSelector, tracks => {
|
t.mapSideEffect(propSelector, tracks => {
|
||||||
console.log("tracks", tracks);
|
|
||||||
if (tracks.length) {
|
if (tracks.length) {
|
||||||
video.srcObject = (tracks[0] as TrackWrapper).stream;
|
video.srcObject = (tracks[0] as TrackWrapper).stream;
|
||||||
}
|
}
|
||||||
|
@ -33,8 +32,8 @@ function bindVideoTracks<T>(t: TemplateBuilder<T>, video: HTMLVideoElement, prop
|
||||||
export class CallView extends TemplateView<CallViewModel> {
|
export class CallView extends TemplateView<CallViewModel> {
|
||||||
render(t: TemplateBuilder<CallViewModel>, vm: CallViewModel): HTMLElement {
|
render(t: TemplateBuilder<CallViewModel>, vm: CallViewModel): HTMLElement {
|
||||||
return t.div({class: "CallView"}, [
|
return t.div({class: "CallView"}, [
|
||||||
t.p(["Call ", vm => vm.name, vm => ` (${vm.id})`]),
|
t.p(vm => `Call ${vm.name} (${vm.id})`),
|
||||||
t.div({class: "CallView_me"}, bindVideoTracks(t, t.video({autoplay: true}), vm => vm.localTracks)),
|
t.div({class: "CallView_me"}, bindVideoTracks(t, t.video({autoplay: true, width: 240}), vm => vm.localTracks)),
|
||||||
t.view(new ListView({list: vm.memberViewModels}, vm => new MemberView(vm)))
|
t.view(new ListView({list: vm.memberViewModels}, vm => new MemberView(vm)))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -42,6 +41,6 @@ export class CallView extends TemplateView<CallViewModel> {
|
||||||
|
|
||||||
class MemberView extends TemplateView<CallMemberViewModel> {
|
class MemberView extends TemplateView<CallMemberViewModel> {
|
||||||
render(t: TemplateBuilder<CallMemberViewModel>, vm: CallMemberViewModel) {
|
render(t: TemplateBuilder<CallMemberViewModel>, vm: CallMemberViewModel) {
|
||||||
return bindVideoTracks(t, t.video({autoplay: true}), vm => vm.tracks);
|
return bindVideoTracks(t, t.video({autoplay: true, width: 360}), vm => vm.tracks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,9 @@ export class CallTileView extends TemplateView<CallTile> {
|
||||||
return t.li(
|
return t.li(
|
||||||
{className: "AnnouncementView"},
|
{className: "AnnouncementView"},
|
||||||
t.div([
|
t.div([
|
||||||
"Call ",
|
vm => vm.label,
|
||||||
vm => vm.name,
|
t.button({className: "CallTileView_join", hidden: vm => !vm.canJoin}, "Join"),
|
||||||
t.button({className: "CallTileView_join"}, "Join")
|
t.button({className: "CallTileView_leave", hidden: vm => !vm.canLeave}, "Leave")
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,8 @@ export class CallTileView extends TemplateView<CallTile> {
|
||||||
onClick(evt) {
|
onClick(evt) {
|
||||||
if (evt.target.className === "CallTileView_join") {
|
if (evt.target.className === "CallTileView_join") {
|
||||||
this.value.join();
|
this.value.join();
|
||||||
|
} else if (evt.target.className === "CallTileView_leave") {
|
||||||
|
this.value.leave();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue