Add voice broadcast playback pip (#9603)

This commit is contained in:
Michael Weimann 2022-11-24 09:08:41 +01:00 committed by GitHub
parent 569a364933
commit a8e15ebe60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 372 additions and 41 deletions

View file

@ -39,11 +39,14 @@ import { CallStore } from "../../../stores/CallStore";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackBody,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingPip,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
} from '../../../voice-broadcast';
import { useCurrentVoiceBroadcastPlayback } from '../../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback';
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@ -57,6 +60,7 @@ const SHOW_CALL_IN_STATES = [
interface IProps {
voiceBroadcastRecording?: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording?: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback?: Optional<VoiceBroadcastPlayback>;
}
interface IState {
@ -330,6 +334,15 @@ class PipView extends React.Component<IProps, IState> {
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
}
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
return ({ onStartMoving }) => <div onMouseDown={onStartMoving}>
<VoiceBroadcastPlaybackBody
playback={voiceBroadcastPlayback}
pip={true}
/>
</div>;
}
private createVoiceBroadcastPreRecordingPipContent(
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
): CreatePipChildren {
@ -358,6 +371,10 @@ class PipView extends React.Component<IProps, IState> {
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
}
if (this.props.voiceBroadcastPlayback) {
pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback);
}
if (this.props.voiceBroadcastRecording) {
pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording);
}
@ -430,9 +447,13 @@ const PipViewHOC: React.FC<IProps> = (props) => {
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
return <PipView
voiceBroadcastRecording={currentVoiceBroadcastRecording}
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
{...props}
/>;
};

View file

@ -30,7 +30,11 @@ import TypingStore from "../stores/TypingStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
import WidgetStore from "../stores/WidgetStore";
import { VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore } from "../voice-broadcast";
import {
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "../voice-broadcast";
export const SDKContext = createContext<SdkContextClass>(undefined);
SDKContext.displayName = "SDKContext";
@ -68,6 +72,7 @@ export class SdkContextClass {
protected _TypingStore?: TypingStore;
protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
protected _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore;
/**
* Automatically construct stores which need to be created eagerly so they can register with
@ -166,4 +171,11 @@ export class SdkContextClass {
}
return this._VoiceBroadcastPreRecordingStore;
}
public get voiceBroadcastPlaybacksStore(): VoiceBroadcastPlaybacksStore {
if (!this._VoiceBroadcastPlaybacksStore) {
this._VoiceBroadcastPlaybacksStore = VoiceBroadcastPlaybacksStore.instance();
}
return this._VoiceBroadcastPlaybacksStore;
}
}

View file

@ -51,6 +51,11 @@ import { UPDATE_EVENT } from "./AsyncStore";
import { SdkContextClass } from "../contexts/SDKContext";
import { CallStore } from "./CallStore";
import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload";
import {
doClearCurrentVoiceBroadcastPlaybackIfStopped,
doMaybeSetCurrentVoiceBroadcastPlayback,
} from "../voice-broadcast";
import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators";
const NUM_JOIN_RETRY = 5;
@ -195,6 +200,28 @@ export class RoomViewStore extends EventEmitter {
this.emit(UPDATE_EVENT);
}
private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void {
doMaybeSetCurrentVoiceBroadcastPlayback(
room,
this.stores.client,
this.stores.voiceBroadcastPlaybacksStore,
this.stores.voiceBroadcastRecordingsStore,
);
}
private onRoomStateEvents(event: MatrixEvent): void {
const roomId = event.getRoomId?.();
// no room or not current room
if (!roomId || roomId !== this.state.roomId) return;
const room = this.stores.client?.getRoom(roomId);
if (room) {
this.doMaybeSetCurrentVoiceBroadcastPlayback(room);
}
}
private onDispatch(payload): void { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) {
// view_room:
@ -219,6 +246,10 @@ export class RoomViewStore extends EventEmitter {
wasContextSwitch: false,
viewingCall: false,
});
doClearCurrentVoiceBroadcastPlaybackIfStopped(this.stores.voiceBroadcastPlaybacksStore);
break;
case "MatrixActions.RoomState.events":
this.onRoomStateEvents((payload as IRoomStateEventsActionPayload).event);
break;
case Action.ViewRoomError:
this.viewRoomError(payload);
@ -395,6 +426,10 @@ export class RoomViewStore extends EventEmitter {
metricsTrigger: payload.metricsTrigger as JoinRoomPayload["metricsTrigger"],
});
}
if (room) {
this.doMaybeSetCurrentVoiceBroadcastPlayback(room);
}
} else if (payload.room_alias) {
// Try the room alias to room ID navigation cache first to avoid
// blocking room navigation on the homeserver.

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { ReactElement } from "react";
import classNames from "classnames";
import {
VoiceBroadcastControl,
@ -36,10 +37,12 @@ import { SeekButton } from "../atoms/SeekButton";
const SEEK_TIME = 30;
interface VoiceBroadcastPlaybackBodyProps {
pip?: boolean;
playback: VoiceBroadcastPlayback;
}
export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProps> = ({
pip = false,
playback,
}) => {
const {
@ -107,8 +110,13 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
/>;
}
const classes = classNames({
mx_VoiceBroadcastBody: true,
["mx_VoiceBroadcastBody--pip"]: pip,
});
return (
<div className="mx_VoiceBroadcastBody">
<div className={classes}>
<VoiceBroadcastHeader
live={liveness}
microphoneLabel={sender?.name}

View file

@ -0,0 +1,44 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useState } from "react";
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
import { VoiceBroadcastPlayback } from "../models/VoiceBroadcastPlayback";
import {
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPlaybacksStoreEvent,
} from "../stores/VoiceBroadcastPlaybacksStore";
export const useCurrentVoiceBroadcastPlayback = (
voiceBroadcastPlaybackStore: VoiceBroadcastPlaybacksStore,
) => {
const [currentVoiceBroadcastPlayback, setVoiceBroadcastPlayback] = useState(
voiceBroadcastPlaybackStore.getCurrent(),
);
useTypedEventEmitter(
voiceBroadcastPlaybackStore,
VoiceBroadcastPlaybacksStoreEvent.CurrentChanged,
(playback: VoiceBroadcastPlayback) => {
setVoiceBroadcastPlayback(playback);
},
);
return {
currentVoiceBroadcastPlayback,
};
};

View file

@ -40,6 +40,8 @@ export * from "./stores/VoiceBroadcastPlaybacksStore";
export * from "./stores/VoiceBroadcastPreRecordingStore";
export * from "./stores/VoiceBroadcastRecordingsStore";
export * from "./utils/checkVoiceBroadcastPreConditions";
export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped";
export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback";
export * from "./utils/getChunkLength";
export * from "./utils/getMaxBroadcastLength";
export * from "./utils/hasRoomLiveVoiceBroadcast";

View file

@ -25,7 +25,7 @@ export enum VoiceBroadcastPlaybacksStoreEvent {
}
interface EventMap {
[VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback) => void;
[VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void;
}
/**
@ -53,7 +53,14 @@ export class VoiceBroadcastPlaybacksStore
this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current);
}
public getCurrent(): VoiceBroadcastPlayback {
public clearCurrent(): void {
if (this.current === null) return;
this.current = null;
this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null);
}
public getCurrent(): VoiceBroadcastPlayback | null {
return this.current;
}
@ -80,11 +87,15 @@ export class VoiceBroadcastPlaybacksStore
state: VoiceBroadcastPlaybackState,
playback: VoiceBroadcastPlayback,
): void => {
if ([
VoiceBroadcastPlaybackState.Buffering,
VoiceBroadcastPlaybackState.Playing,
].includes(state)) {
this.pauseExcept(playback);
switch (state) {
case VoiceBroadcastPlaybackState.Buffering:
case VoiceBroadcastPlaybackState.Playing:
this.pauseExcept(playback);
this.setCurrent(playback);
break;
case VoiceBroadcastPlaybackState.Stopped:
this.clearCurrent();
break;
}
};

View file

@ -0,0 +1,26 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from "..";
export const doClearCurrentVoiceBroadcastPlaybackIfStopped = (
voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore,
) => {
if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) {
// clear current if stopped
return;
}
};

View file

@ -0,0 +1,64 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import {
hasRoomLiveVoiceBroadcast,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPlaybackState,
VoiceBroadcastRecordingsStore,
} from "..";
/**
* When a live voice broadcast is in the room and
* another voice broadcast is not currently being listened to or recorded
* the live broadcast in the room is set as the current broadcast to listen to.
* When there is no live broadcast in the room: clear current broadcast.
*
* @param {Room} room The room to check for a live voice broadcast
* @param {MatrixClient} client
* @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore
* @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore
*/
export const doMaybeSetCurrentVoiceBroadcastPlayback = (
room: Room,
client: MatrixClient,
voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore,
voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore,
): void => {
// do not disturb the current recording
if (voiceBroadcastRecordingsStore.hasCurrent()) return;
const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent();
if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) {
// do not disturb the current playback
return;
}
const { infoEvent } = hasRoomLiveVoiceBroadcast(room);
if (infoEvent) {
// live broadcast in the room + no recording + not listening yet: set the current broadcast
const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client);
voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
return;
}
// no broadcast; not listening: clear current
voiceBroadcastPlaybacksStore.clearCurrent();
};

View file

@ -19,36 +19,42 @@ import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
interface Result {
// whether there is a live broadcast in the room
hasBroadcast: boolean;
// info event of any live broadcast in the room
infoEvent: MatrixEvent | null;
// whether the broadcast was started by the user
startedByUser: boolean;
}
/**
* Finds out whether there is a live broadcast in a room.
* Also returns if the user started the broadcast (if any).
*/
export const hasRoomLiveVoiceBroadcast = (room: Room, userId: string): Result => {
export const hasRoomLiveVoiceBroadcast = (room: Room, userId?: string): Result => {
let hasBroadcast = false;
let startedByUser = false;
let infoEvent: MatrixEvent | null = null;
const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType);
stateEvents.forEach((event: MatrixEvent) => {
stateEvents.every((event: MatrixEvent) => {
const state = event.getContent()?.state;
if (state && state !== VoiceBroadcastInfoState.Stopped) {
hasBroadcast = true;
infoEvent = event;
// state key = sender's MXID
if (event.getStateKey() === userId) {
infoEvent = event;
startedByUser = true;
// break here, because more than true / true is not possible
return false;
}
}
return true;
});
return {
hasBroadcast,
infoEvent,
startedByUser,
};
};