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,
     });
 }