Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Add voice broadcast max length
Browse files Browse the repository at this point in the history
  • Loading branch information
weeman1337 committed Nov 10, 2022
1 parent 962e8e0 commit 3f6aca3
Show file tree
Hide file tree
Showing 22 changed files with 469 additions and 145 deletions.
3 changes: 3 additions & 0 deletions res/img/element-icons/Timer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,31 @@ export function formatSeconds(inSeconds: number): string {
return output;
}

export function formatTimeLeft(inSeconds: number): string {
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0);
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0);
const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0);

if (hours !== "0") {
return _t("%(hours)sh %(minutes)sm %(seconds)ss left", {
hours,
minutes,
seconds,
});
}

if (minutes !== "0") {
return _t("%(minutes)sm %(seconds)ss left", {
minutes,
seconds,
});
}

return _t("%(seconds)ss left", {
seconds,
});
}

const MILLIS_IN_DAY = 86400000;
function withinPast24Hours(prevDate: Date, nextDate: Date): boolean {
return Math.abs(prevDate.getTime() - nextDate.getTime()) <= MILLIS_IN_DAY;
Expand Down
2 changes: 2 additions & 0 deletions src/IConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ export interface IConfigOptions {
voice_broadcast?: {
// length per voice chunk in seconds
chunk_length?: number;
// max voice broadcast length in seconds
max_length?: number;
};

user_notice?: {
Expand Down
3 changes: 2 additions & 1 deletion src/SdkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export const DEFAULTS: IConfigOptions = {
url: "https://element.io/get-started",
},
voice_broadcast: {
chunk_length: 120, // two minutes
chunk_length: 2 * 60, // two minutes
max_length: 4 * 60 * 60, // four hours
},
};

Expand Down
20 changes: 13 additions & 7 deletions src/components/views/audio_messages/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,34 @@ import React, { HTMLProps } from "react";

import { formatSeconds } from "../../../DateUtils";

interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
interface Props extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
seconds: number;
formatFn?: (seconds: number) => string;
}

/**
* Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29".
* Simply converts seconds using formatFn.
* Defaulting to formatSeconds().
* Note that in this case hours will not be displayed, making it possible to see "82:29".
*/
export default class Clock extends React.Component<IProps> {
public constructor(props) {
export default class Clock extends React.Component<Props> {
public static defaultProps = {
formatFn: formatSeconds,
};

public constructor(props: Props) {
super(props);
}

public shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
public shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
const currentFloor = Math.floor(this.props.seconds);
const nextFloor = Math.floor(nextProps.seconds);
return currentFloor !== nextFloor;
}

public render() {
return <span aria-live={this.props["aria-live"]} role={this.props.role} className='mx_Clock'>
{ formatSeconds(this.props.seconds) }
{ this.props.formatFn(this.props.seconds) }
</span>;
}
}
4 changes: 3 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"%(hours)sh %(minutes)sm %(seconds)ss left": "%(hours)sh %(minutes)sm %(seconds)ss left",
"%(minutes)sm %(seconds)ss left": "%(minutes)sm %(seconds)ss left",
"%(seconds)ss left": "%(seconds)ss left",
"%(date)s at %(time)s": "%(date)s at %(time)s",
"%(value)sd": "%(value)sd",
"%(value)sh": "%(value)sh",
Expand Down Expand Up @@ -1886,7 +1889,6 @@
"The conversation continues here.": "The conversation continues here.",
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
"You do not have permission to post to this room": "You do not have permission to post to this room",
"%(seconds)ss left": "%(seconds)ss left",
"Send voice message": "Send voice message",
"Hide stickers": "Hide stickers",
"Sticker": "Sticker",
Expand Down
35 changes: 28 additions & 7 deletions src/voice-broadcast/audio/VoiceBroadcastRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ import { Optional } from "matrix-events-sdk";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";

import { getChunkLength } from "..";
import { VoiceRecording } from "../../audio/VoiceRecording";
import { IRecordingUpdate, VoiceRecording } from "../../audio/VoiceRecording";
import { concat } from "../../utils/arrays";
import { IDestroyable } from "../../utils/IDestroyable";
import { Singleflight } from "../../utils/Singleflight";

export enum VoiceBroadcastRecorderEvent {
ChunkRecorded = "chunk_recorded",
CurrentChunkLengthUpdated = "current_chunk_length_updated",
}

interface EventMap {
[VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void;
[VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated]: (length: number) => void;
}

export interface ChunkRecordedPayload {
Expand All @@ -46,8 +48,11 @@ export class VoiceBroadcastRecorder
implements IDestroyable {
private headers = new Uint8Array(0);
private chunkBuffer = new Uint8Array(0);
// position of the previous chunk in seconds
private previousChunkEndTimePosition = 0;
private pagesFromRecorderCount = 0;
// current chunk length in seconds
private currentChunkLength = 0;

public constructor(
private voiceRecording: VoiceRecording,
Expand All @@ -58,7 +63,11 @@ export class VoiceBroadcastRecorder
}

public async start(): Promise<void> {
return this.voiceRecording.start();
await this.voiceRecording.start();
this.voiceRecording.liveData.onUpdate((data: IRecordingUpdate) => {
this.setCurrentChunkLength(data.timeSeconds - this.previousChunkEndTimePosition);
});
return;
}

/**
Expand All @@ -68,15 +77,25 @@ export class VoiceBroadcastRecorder
await this.voiceRecording.stop();
// forget about that call, so that we can stop it again later
Singleflight.forgetAllFor(this.voiceRecording);
return this.extractChunk();
const chunk = this.extractChunk();
this.currentChunkLength = 0;
this.previousChunkEndTimePosition = 0;
return chunk;
}

public get contentType(): string {
return this.voiceRecording.contentType;
}

private get chunkLength(): number {
return this.voiceRecording.recorderSeconds - this.previousChunkEndTimePosition;
private setCurrentChunkLength(currentChunkLength: number): void {
if (this.currentChunkLength === currentChunkLength) return;

this.currentChunkLength = currentChunkLength;
this.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, currentChunkLength);
}

public getCurrentChunkLength(): number {
return this.currentChunkLength;
}

private onDataAvailable = (data: ArrayBuffer): void => {
Expand All @@ -89,6 +108,7 @@ export class VoiceBroadcastRecorder
return;
}

this.setCurrentChunkLength(this.voiceRecording.recorderSeconds - this.previousChunkEndTimePosition);
this.handleData(dataArray);
};

Expand All @@ -98,7 +118,7 @@ export class VoiceBroadcastRecorder
}

private emitChunkIfTargetLengthReached(): void {
if (this.chunkLength >= this.targetChunkLength) {
if (this.getCurrentChunkLength() >= this.targetChunkLength) {
this.emitAndResetChunk();
}
}
Expand All @@ -114,9 +134,10 @@ export class VoiceBroadcastRecorder
const currentRecorderTime = this.voiceRecording.recorderSeconds;
const payload: ChunkRecordedPayload = {
buffer: concat(this.headers, this.chunkBuffer),
length: this.chunkLength,
length: this.getCurrentChunkLength(),
};
this.chunkBuffer = new Uint8Array(0);
this.setCurrentChunkLength(0);
this.previousChunkEndTimePosition = currentRecorderTime;
return payload;
}
Expand Down
13 changes: 13 additions & 0 deletions src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { LiveBadge } from "../..";
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
import { Icon as MicrophoneIcon } from "../../../../res/img/voip/call-view/mic-on.svg";
import { Icon as TimerIcon } from "../../../../res/img/element-icons/Timer.svg";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../../../components/views/avatars/RoomAvatar";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import { Icon as XIcon } from "../../../../res/img/element-icons/cancel-rounded.svg";
import Clock from "../../../components/views/audio_messages/Clock";
import { formatTimeLeft } from "../../../DateUtils";

interface VoiceBroadcastHeaderProps {
live?: boolean;
onCloseClick?: () => void;
room: Room;
sender: RoomMember;
showBroadcast?: boolean;
timeLeft?: number;
showClose?: boolean;
}

Expand All @@ -38,6 +42,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
sender,
showBroadcast = false,
showClose = false,
timeLeft,
}) => {
const broadcast = showBroadcast
? <div className="mx_VoiceBroadcastHeader_line">
Expand All @@ -54,6 +59,13 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
</AccessibleButton>
: null;

const timeLeftLine = timeLeft
? <div className="mx_VoiceBroadcastHeader_line">
<TimerIcon className="mx_Icon mx_Icon_16" />
<Clock formatFn={formatTimeLeft} seconds={timeLeft} />
</div>
: null;

return <div className="mx_VoiceBroadcastHeader">
<RoomAvatar room={room} width={32} height={32} />
<div className="mx_VoiceBroadcastHeader_content">
Expand All @@ -64,6 +76,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
<span>{ sender.name }</span>
</div>
{ timeLeftLine }
{ broadcast }
</div>
{ liveBadge }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface VoiceBroadcastRecordingPipProps {
export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => {
const {
live,
timeLeft,
recordingState,
room,
sender,
Expand All @@ -58,6 +59,7 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
live={live}
sender={sender}
room={room}
timeLeft={timeLeft}
/>
<hr className="mx_VoiceBroadcastBody_divider" />
<div className="mx_VoiceBroadcastBody_controls">
Expand Down
8 changes: 8 additions & 0 deletions src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
},
);

const [timeLeft, setTimeLeft] = useState(recording.getTimeLeft());
useTypedEventEmitter(
recording,
VoiceBroadcastRecordingEvent.TimeLeftChanged,
setTimeLeft,
);

const live = [
VoiceBroadcastInfoState.Started,
VoiceBroadcastInfoState.Paused,
Expand All @@ -73,6 +80,7 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =

return {
live,
timeLeft,
recordingState,
room,
sender: recording.infoEvent.sender,
Expand Down
1 change: 1 addition & 0 deletions src/voice-broadcast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from "./stores/VoiceBroadcastPreRecordingStore";
export * from "./stores/VoiceBroadcastRecordingsStore";
export * from "./utils/checkVoiceBroadcastPreConditions";
export * from "./utils/getChunkLength";
export * from "./utils/getMaxBroadcastLength";
export * from "./utils/hasRoomLiveVoiceBroadcast";
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";
Expand Down
Loading

0 comments on commit 3f6aca3

Please sign in to comment.