From 20e57d15fd2d77e9dc783ef55480a963a5329054 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Tue, 2 Mar 2021 15:20:54 +0000 Subject: [PATCH 1/7] Option for audio streaming --- .../views/context_menus/WidgetContextMenu.tsx | 13 +++++++++++++ src/i18n/strings/en_EN.json | 1 + src/stores/widgets/ElementWidgetActions.ts | 1 + 3 files changed, 15 insertions(+) diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index c1af86eae6..e7d1c02c66 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -31,6 +31,7 @@ import QuestionDialog from "../dialogs/QuestionDialog"; import {WidgetType} from "../../../widgets/WidgetType"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; +import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; interface IProps extends React.ComponentProps<typeof IconizedContextMenu> { app: IApp; @@ -54,6 +55,17 @@ const WidgetContextMenu: React.FC<IProps> = ({ const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId); + let streamAudioStreamButton; + if (getConfigLivestreamUrl() && (app.type === "m.jitsi" || app.type === "jitsi")) { + const onStreamAudioClick = () => { + startJitsiAudioLivestream(widgetMessaging, roomId); + onFinished(); + }; + streamAudioStreamButton = <IconizedContextMenuOption + onClick={onStreamAudioClick} label={_t("Start audio stream")} + />; + } + let unpinButton; if (showUnpin) { const onUnpinClick = () => { @@ -163,6 +175,7 @@ const WidgetContextMenu: React.FC<IProps> = ({ return <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}> <IconizedContextMenuOptionList> + { streamAudioStreamButton } { editButton } { revokeButton } { deleteButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 38460a5f6e..7242ed7de6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2357,6 +2357,7 @@ "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", + "Start audio stream": "Start audio stream", "Take a picture": "Take a picture", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts index de48746a74..cd591a6fb4 100644 --- a/src/stores/widgets/ElementWidgetActions.ts +++ b/src/stores/widgets/ElementWidgetActions.ts @@ -19,6 +19,7 @@ import { IWidgetApiRequest } from "matrix-widget-api"; export enum ElementWidgetActions { ClientReady = "im.vector.ready", HangupCall = "im.vector.hangup", + StartLiveStream = "im.vector.start_live_stream", OpenIntegrationManager = "integration_manager_open", /** From 63944b9f6da84dc4b385cb3f7f3b995cabab56e4 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Thu, 4 Mar 2021 12:22:31 +0000 Subject: [PATCH 2/7] Add the new file --- src/Livestream.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/Livestream.ts diff --git a/src/Livestream.ts b/src/Livestream.ts new file mode 100644 index 0000000000..d4bed63dbd --- /dev/null +++ b/src/Livestream.ts @@ -0,0 +1,52 @@ +/* +Copyright 2021 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 { ClientWidgetApi } from "matrix-widget-api"; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import SdkConfig from "./SdkConfig"; +import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; + +export function getConfigLivestreamUrl() { + return SdkConfig.get()["audioStreamUrl"]; +} + +async function createLiveStream(roomId: string) { + const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); + + const url = getConfigLivestreamUrl() + "/createStream"; + + const response = await window.fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + room_id: roomId, + openid_token: openIdToken, + }), + }); + + const respBody = response.json(); + return respBody['stream_id']; +} + +export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) { + const streamId = await createLiveStream(roomId); + + widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { + rtmpStreamKey: 'audioStream:' + streamId, + }); +} From 0f1b7a001e1b943ff28acc664b009264cc7fd588 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Thu, 4 Mar 2021 17:52:49 +0000 Subject: [PATCH 3/7] Better error handling for streams Also use older youtubeStreamKey as it appears our jitsi doesn't support the newer one. --- src/Livestream.ts | 6 +++--- .../views/context_menus/WidgetContextMenu.tsx | 15 +++++++++++++-- src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Livestream.ts b/src/Livestream.ts index d4bed63dbd..cd8cdea179 100644 --- a/src/Livestream.ts +++ b/src/Livestream.ts @@ -39,14 +39,14 @@ async function createLiveStream(roomId: string) { }), }); - const respBody = response.json(); + const respBody = await response.json(); return respBody['stream_id']; } export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) { const streamId = await createLiveStream(roomId); - widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { - rtmpStreamKey: 'audioStream:' + streamId, + await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { + rtmpStreamKey: 'rtmp://audiostream.dummy/' + streamId, }); } diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index e7d1c02c66..0503df038a 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -28,6 +28,7 @@ import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; import QuestionDialog from "../dialogs/QuestionDialog"; +import ErrorDialog from "../dialogs/ErrorDialog"; import {WidgetType} from "../../../widgets/WidgetType"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; @@ -57,8 +58,18 @@ const WidgetContextMenu: React.FC<IProps> = ({ let streamAudioStreamButton; if (getConfigLivestreamUrl() && (app.type === "m.jitsi" || app.type === "jitsi")) { - const onStreamAudioClick = () => { - startJitsiAudioLivestream(widgetMessaging, roomId); + const onStreamAudioClick = async () => { + try { + await startJitsiAudioLivestream(widgetMessaging, roomId); + } catch (err) { + console.log("Failed to start livestream", err); + // XXX: won't i18n well, but looks like widget api only support 'message'? + const message = err.message || _t("Unable to start audio streaming."); + Modal.createTrackedDialog('WidgetContext Menu', 'Livestream failed', ErrorDialog, { + title: _t('Failed to start livestream'), + description: message, + }); + } onFinished(); }; streamAudioStreamButton = <IconizedContextMenuOption diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7242ed7de6..f33c4368ef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2357,6 +2357,8 @@ "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", + "Failed to start livestream": "Failed to start livestream", + "Unable to start audio streaming.": "Unable to start audio streaming.", "Start audio stream": "Start audio stream", "Take a picture": "Take a picture", "Delete Widget": "Delete Widget", From aaf653dd8f0f616792569b888224958d9be99fba Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Thu, 4 Mar 2021 17:58:43 +0000 Subject: [PATCH 4/7] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f33c4368ef..2183a0c68f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2357,8 +2357,8 @@ "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", - "Failed to start livestream": "Failed to start livestream", "Unable to start audio streaming.": "Unable to start audio streaming.", + "Failed to start livestream": "Failed to start livestream", "Start audio stream": "Start audio stream", "Take a picture": "Take a picture", "Delete Widget": "Delete Widget", From c80cbc38dd28e89d1b643189acb9c659914989dc Mon Sep 17 00:00:00 2001 From: David Baker <dbkr@users.noreply.github.com> Date: Fri, 5 Mar 2021 10:32:54 +0000 Subject: [PATCH 5/7] Use helper class (It did not need imports) Co-authored-by: Travis Ralston <travpc@gmail.com> --- src/components/views/context_menus/WidgetContextMenu.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 0503df038a..03e63edbcc 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({ const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId); let streamAudioStreamButton; - if (getConfigLivestreamUrl() && (app.type === "m.jitsi" || app.type === "jitsi")) { + if (getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type)) { const onStreamAudioClick = async () => { try { await startJitsiAudioLivestream(widgetMessaging, roomId); @@ -199,4 +199,3 @@ const WidgetContextMenu: React.FC<IProps> = ({ }; export default WidgetContextMenu; - From 8bcf0f08385368ce0dce69510e814f478aa90e2c Mon Sep 17 00:00:00 2001 From: David Baker <dbkr@users.noreply.github.com> Date: Fri, 5 Mar 2021 10:34:03 +0000 Subject: [PATCH 6/7] console.error Co-authored-by: Travis Ralston <travpc@gmail.com> --- src/components/views/context_menus/WidgetContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 03e63edbcc..623fe04f2f 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -62,7 +62,7 @@ const WidgetContextMenu: React.FC<IProps> = ({ try { await startJitsiAudioLivestream(widgetMessaging, roomId); } catch (err) { - console.log("Failed to start livestream", err); + console.error("Failed to start livestream", err); // XXX: won't i18n well, but looks like widget api only support 'message'? const message = err.message || _t("Unable to start audio streaming."); Modal.createTrackedDialog('WidgetContext Menu', 'Livestream failed', ErrorDialog, { From 572f15522f28320f671980a5892ec2cf10fd0d8e Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 5 Mar 2021 10:40:11 +0000 Subject: [PATCH 7/7] use a constant --- src/Livestream.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Livestream.ts b/src/Livestream.ts index cd8cdea179..2389132762 100644 --- a/src/Livestream.ts +++ b/src/Livestream.ts @@ -23,6 +23,9 @@ export function getConfigLivestreamUrl() { return SdkConfig.get()["audioStreamUrl"]; } +// Dummy rtmp URL used to signal that we want a special audio-only stream +const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/'; + async function createLiveStream(roomId: string) { const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); @@ -47,6 +50,6 @@ export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi const streamId = await createLiveStream(roomId); await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { - rtmpStreamKey: 'rtmp://audiostream.dummy/' + streamId, + rtmpStreamKey: AUDIOSTREAM_DUMMY_URL + streamId, }); }