Add voice broadcast playback pip (#9603)
This commit is contained in:
parent
569a364933
commit
a8e15ebe60
13 changed files with 372 additions and 41 deletions
|
@ -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}
|
||||
/>;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue