diff --git a/src/domain/session/room/CallViewModel.ts b/src/domain/session/room/CallViewModel.ts index 37f30840..358b26f0 100644 --- a/src/domain/session/room/CallViewModel.ts +++ b/src/domain/session/room/CallViewModel.ts @@ -39,7 +39,11 @@ export class CallViewModel extends ViewModel { constructor(options: Options) { super(options); - const ownMemberViewModelMap = new ObservableValueMap("self", new EventObservableValue(this.call, "change")) + const callObservable = new EventObservableValue(this.call, "change"); + this.track(callObservable.subscribe(() => { + this.emitChange(); + })); + const ownMemberViewModelMap = new ObservableValueMap("self", callObservable) .mapValues((call, emitChange) => new OwnMemberViewModel(this.childOptions({call, emitChange})), () => {}); this.memberViewModels = this.call.members .filterValues(member => member.isConnected) @@ -79,6 +83,10 @@ export class CallViewModel extends ViewModel { return this.call.id; } + get error(): string | undefined { + return this.call.error?.message; + } + private get call(): GroupCall { return this.getOption("call"); } @@ -135,6 +143,10 @@ class OwnMemberViewModel extends ViewModel implements IStreamViewModel })); } + get error(): string | undefined { + return undefined; + } + get stream(): Stream | undefined { return this.call.localPreviewMedia?.userMedia; } @@ -195,6 +207,10 @@ export class CallMemberViewModel extends ViewModel implements ISt return this.member.remoteMedia?.userMedia; } + get error(): string | undefined { + return this.member.error?.message; + } + private get member(): Member { return this.getOption("member"); } @@ -242,4 +258,5 @@ export interface IStreamViewModel extends AvatarSource, ViewModel { get stream(): Stream | undefined; get isCameraMuted(): boolean; get isMicrophoneMuted(): boolean; + get error(): string | undefined; } diff --git a/src/domain/session/room/timeline/tiles/CallTile.js b/src/domain/session/room/timeline/tiles/CallTile.js index 0bc12698..a54af5d8 100644 --- a/src/domain/session/room/timeline/tiles/CallTile.js +++ b/src/domain/session/room/timeline/tiles/CallTile.js @@ -74,9 +74,14 @@ export class CallTile extends SimpleTile { async join() { if (this.canJoin) { - const stream = await this.platform.mediaDevices.getMediaTracks(false, true); - const localMedia = new LocalMedia().withUserMedia(stream); - await this._call.join(localMedia); + try { + const stream = await this.platform.mediaDevices.getMediaTracks(false, true); + const localMedia = new LocalMedia().withUserMedia(stream); + await this._call.join(localMedia); + } catch (err) { + this._error = err; + this.emitChange("error"); + } } } diff --git a/src/platform/web/ui/css/themes/element/call.css b/src/platform/web/ui/css/themes/element/call.css index 10388fb4..f4d674b4 100644 --- a/src/platform/web/ui/css/themes/element/call.css +++ b/src/platform/web/ui/css/themes/element/call.css @@ -24,6 +24,14 @@ limitations under the License. grid-row: 1; } +.CallView_error { + color: red; + font-weight: bold; + align-self: start; + justify-self: center; + margin: 16px; +} + .CallView_members { display: grid; gap: 12px; @@ -59,6 +67,14 @@ limitations under the License. justify-self: center; } +.StreamView_error { + color: red; + font-weight: bold; + align-self: start; + justify-self: center; + margin: 16px; +} + .StreamView_muteStatus { align-self: start; justify-self: end; diff --git a/src/platform/web/ui/session/room/CallView.ts b/src/platform/web/ui/session/room/CallView.ts index 619afc2e..87cac99f 100644 --- a/src/platform/web/ui/session/room/CallView.ts +++ b/src/platform/web/ui/session/room/CallView.ts @@ -43,7 +43,10 @@ export class CallView extends TemplateView { "CallView_unmutedCamera": vm => !vm.isCameraMuted, }, onClick: disableTargetCallback(() => vm.toggleCamera())}), t.button({className: "CallView_hangup", onClick: disableTargetCallback(() => vm.hangup())}), - ]) + ]), + t.if(vm => !!vm.error, t => { + return t.div({className: "CallView_error"}, vm => vm.error); + }) ]); } @@ -112,6 +115,9 @@ class StreamView extends TemplateView { microphoneMuted: vm => vm.isMicrophoneMuted && !vm.isCameraMuted, cameraMuted: vm => vm.isCameraMuted, } + }), + t.if(vm => !!vm.error, t => { + return t.div({className: "StreamView_error"}, vm => vm.error); }) ]); }