diff --git a/docs/labs.md b/docs/labs.md
index e6e74db94c..72b8b09bae 100644
--- a/docs/labs.md
+++ b/docs/labs.md
@@ -183,6 +183,6 @@ Threads can be access by clicking their summary below the root event on the room
 
 This feature might work in degraded mode if the homeserver a user is connected to does not advertise support for the unstable feature `org.matrix.msc3440`  when calling the `/versions` API endpoint.
 
-## Voice & video rooms (`feature_voice_rooms`) [In Development]
+## Voice & video rooms (`feature_video_rooms`) [In Development]
 
-Enables support for creating and joining voice & video rooms, which are persistent voice chats that users can jump in and out of.
+Enables support for creating and joining video rooms, which are persistent video chats that users can jump in and out of.
diff --git a/docs/translating.md b/docs/translating.md
index a8d29a387d..bfb9702751 100644
--- a/docs/translating.md
+++ b/docs/translating.md
@@ -62,4 +62,4 @@ You can use inside the translation field "Review needed" checkbox. It will be sh
 
 ### Further reading
 
-The official Weblate doc provides some more in-deepth explanation on how to do translations and talks about do and don'ts. You can find it at: https://docs.weblate.org/en/latest/user/translating.html
+The official Weblate doc provides some more in-depth explanation on how to do translations and talks about do and don'ts. You can find it at: https://docs.weblate.org/en/latest/user/translating.html
\ No newline at end of file
diff --git a/src/vector/jitsi/index.scss b/src/vector/jitsi/index.scss
index 95a23c1772..ac6aff1652 100644
--- a/src/vector/jitsi/index.scss
+++ b/src/vector/jitsi/index.scss
@@ -56,6 +56,10 @@ body, html {
     position: absolute;
     height: 100%;
     width: 100%;
+
+    // Hidden by default to avoid flashing the prejoin screen at the user when
+    // we're supposed to skip it anyways
+    visibility: hidden;
 }
 
 .joinConferenceFloating {
diff --git a/src/vector/jitsi/index.ts b/src/vector/jitsi/index.ts
index d62b76f5ac..055d4aff4f 100644
--- a/src/vector/jitsi/index.ts
+++ b/src/vector/jitsi/index.ts
@@ -51,6 +51,7 @@ let roomId: string;
 let openIdToken: IOpenIDCredentials;
 let roomName: string;
 let startAudioOnly: boolean;
+let isVideoChannel: boolean;
 
 let widgetApi: WidgetApi;
 let meetApi: any; // JitsiMeetExternalAPI
@@ -120,18 +121,17 @@ const ack = (ev: CustomEvent<IWidgetApiRequest>) => widgetApi.transport.reply(ev
         roomId = qsParam('roomId', true);
         roomName = qsParam('roomName', true);
         startAudioOnly = qsParam('isAudioOnly', true) === "true";
+        isVideoChannel = qsParam('isVideoChannel', true) === "true";
 
         // We've reached the point where we have to wait for the config, so do that then parse it.
         const instanceConfig = new SnakedObject<IConfigOptions>((await configPromise) ?? <IConfigOptions>{});
         const jitsiConfig = instanceConfig.get("jitsi_widget") ?? {};
         skipOurWelcomeScreen = (new SnakedObject<IConfigOptions["jitsi_widget"]>(jitsiConfig))
-            .get("skip_built_in_welcome_screen") || false;
+            .get("skip_built_in_welcome_screen") || isVideoChannel;
 
-        // If we're meant to skip our screen, skip to the part where we show Jitsi instead of us.
+        // Either reveal the prejoin screen, or skip straight to Jitsi depending on the config.
         // We don't set up the call yet though as this might lead to failure without the widget API.
-        if (skipOurWelcomeScreen) {
-            toggleConferenceVisibility(true);
-        }
+        toggleConferenceVisibility(skipOurWelcomeScreen);
 
         if (widgetApi) {
             await readyPromise;
@@ -300,6 +300,7 @@ function joinConference() { // event handler bound in HTML
         "they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " +
         "our fragment values and not recognizing the options.",
     );
+
     const options = {
         width: "100%",
         height: "100%",
@@ -313,10 +314,23 @@ function joinConference() { // event handler bound in HTML
         },
         configOverwrite: {
             startAudioOnly,
-        },
+        } as any,
         jwt: jwt,
     };
 
+    // Video channel widgets need some more tailored config options
+    if (isVideoChannel) {
+        // Ensure that we start on Jitsi Meet's native prejoin screen, for
+        // deployments that skip straight to the conference by default
+        options.configOverwrite.prejoinConfig = { enabled: true };
+        // Use a simplified set of toolbar buttons
+        options.configOverwrite.toolbarButtons = [
+            "microphone", "camera", "desktop", "tileview", "hangup",
+        ];
+        // Hide all top bar elements
+        options.configOverwrite.conferenceInfo = { autoHide: [] };
+    }
+
     meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
     if (displayName) meetApi.executeCommand("displayName", displayName);
     if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
@@ -332,6 +346,9 @@ function joinConference() { // event handler bound in HTML
             widgetApi.setAlwaysOnScreen(true);
             widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
         }
+
+        // Video rooms should start in tile mode
+        if (isVideoChannel) meetApi.executeCommand("setTileView", true);
     });
 
     meetApi.on("readyToClose", () => {
@@ -342,7 +359,7 @@ function joinConference() { // event handler bound in HTML
             // can cause the receiving side to instantly stop listening.
             // ignored promise because we don't care if it works
             // noinspection JSIgnoredPromiseFromCall
-            widgetApi.transport.send(ElementWidgetActions.HangupCall, {}).then(() =>
+            widgetApi.transport.send(ElementWidgetActions.HangupCall, {}).finally(() =>
                 widgetApi.setAlwaysOnScreen(false),
             );
         }