feat(shared-video): add video source types, embed URLs, and tile view support

Adds VIDEO_SOURCE_TYPES, getVideoSourceType, getVideoEmbedUrl to
shared-video for multi-platform embed support (Vimeo, Dailymotion,
Twitch). Adds SharedVideoTile and EmbedPlayerManager components.
Updates shared-music with embed player manager and source type helpers.
CSS fixes for meeting intelligence, toolbars, and filmstrip tiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-10 18:58:10 -04:00
parent 1706dc47a3
commit 8dc9c74c45
18 changed files with 421 additions and 18 deletions

View File

@ -106,7 +106,7 @@ form {
.watermark {
display: block;
position: absolute;
top: 15;
top: 15px;
width: $watermarkWidth;
height: $watermarkHeight;
background-size: contain;

View File

@ -7,6 +7,7 @@
right: 0;
top: 0;
width: 400px;
max-width: 100%;
height: 100%;
background: rgba(28, 32, 37, 0.98);
border-left: 1px solid rgba(255, 255, 255, 0.1);
@ -14,6 +15,7 @@
flex-direction: column;
z-index: 350;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
padding-bottom: env(safe-area-inset-bottom, 0);
}
.meeting-intelligence-header {
@ -42,8 +44,8 @@
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
padding: 4px;
border-radius: 4px;
padding: 10px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;

View File

@ -93,6 +93,7 @@
.toolbox-content-wrapper::after {
content: '';
display: block;
background: $newToolbarBackgroundColor;
padding-bottom: env(safe-area-inset-bottom, 0);
}

View File

@ -29,7 +29,7 @@
}
.layout-indicator {
bottom: 80px;
bottom: calc(100px + env(safe-area-inset-bottom, 0px));
display: flex;
gap: 6px;
justify-content: center;
@ -37,13 +37,13 @@
pointer-events: none;
position: absolute;
right: 0;
z-index: 2;
z-index: 251;
}
.layout-indicator-dot {
border-radius: 50%;
height: 6px;
width: 6px;
height: 8px;
width: 8px;
}
.layout-indicator-dot--active {
@ -74,7 +74,7 @@
#localVideoWrapper video,
#localVideoWrapper object {
border-radius: $borderRadius !important;
cursor: hand;
cursor: pointer;
object-fit: cover;
}

View File

@ -7,11 +7,11 @@
margin: 0 2px;
&:hover {
cursor: hand;
cursor: pointer;
}
& > video {
cursor: hand;
cursor: pointer;
border-radius: $borderRadius;
object-fit: cover;
overflow: hidden;

View File

@ -3,8 +3,8 @@
<!--#include virtual="head.html" -->
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="theme-color" content="#2A3A4B">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#1e1040">
<!--#include virtual="base.html" -->
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">

110
package-lock.json generated
View File

@ -55,6 +55,7 @@
"dayjs": "1.11.13",
"dompurify": "3.2.6",
"dropbox": "10.7.0",
"emoji-mart": "^5.6.0",
"focus-visible": "5.1.0",
"glob": "11.0.3",
"grapheme-splitter": "1.0.4",
@ -132,6 +133,7 @@
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.4",
"@playwright/test": "^1.58.2",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
@ -5247,6 +5249,22 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.21",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@ -13088,6 +13106,12 @@
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==",
"license": "ISC"
},
"node_modules/emoji-mart": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -21198,6 +21222,53 @@
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
@ -30480,6 +30551,15 @@
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
"dev": true
},
"@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"requires": {
"playwright": "1.58.2"
}
},
"@polka/url": {
"version": "1.0.0-next.21",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@ -36018,6 +36098,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz",
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ=="
},
"emoji-mart": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -41782,6 +41867,31 @@
"find-up": "^4.0.0"
}
},
"playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.58.2"
},
"dependencies": {
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
}
}
},
"playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true
},
"pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",

View File

@ -61,6 +61,7 @@
"dayjs": "1.11.13",
"dompurify": "3.2.6",
"dropbox": "10.7.0",
"emoji-mart": "^5.6.0",
"focus-visible": "5.1.0",
"glob": "11.0.3",
"grapheme-splitter": "1.0.4",
@ -138,6 +139,7 @@
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.4",
"@playwright/test": "^1.58.2",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",

View File

@ -3,6 +3,7 @@ import React from 'react';
import GlobalStyles from '../../base/ui/components/GlobalStyles.web';
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web';
import DialogContainer from '../../base/ui/components/web/DialogContainer';
import DialogErrorBoundary from '../../base/ui/components/web/DialogErrorBoundary';
import ChromeExtensionBanner from '../../chrome-extension-banner/components/ChromeExtensionBanner.web';
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
import PiP from '../../pip/components/PiP';
@ -62,7 +63,9 @@ export class App extends AbstractApp {
override _renderDialogContainer() {
return (
<JitsiThemeProvider>
<DialogContainer />
<DialogErrorBoundary>
<DialogContainer />
</DialogErrorBoundary>
</JitsiThemeProvider>
);
}

View File

@ -72,6 +72,35 @@ function getEmbedUrl(url: string, sourceType: string): string {
return url;
}
case SOURCE_TYPES.APPLE_MUSIC: {
// Replace music.apple.com with embed.music.apple.com
return url.replace('music.apple.com', 'embed.music.apple.com');
}
case SOURCE_TYPES.DEEZER: {
// Extract type and ID: deezer.com/{country}/track/123
const deezerMatch = url.match(/deezer\.com\/(?:[a-z]{2}\/)?(track|album|playlist)\/(\d+)/);
if (deezerMatch) {
return `https://widget.deezer.com/widget/dark/${deezerMatch[1]}/${deezerMatch[2]}`;
}
return url;
}
case SOURCE_TYPES.TIDAL: {
// Extract type and ID: tidal.com/browse/track/123
const tidalMatch = url.match(/tidal\.com\/(?:browse\/)?(track|album|playlist|mix)\/([a-zA-Z0-9-]+)/);
if (tidalMatch) {
return `https://embed.tidal.com/${tidalMatch[1]}s/${tidalMatch[2]}`;
}
return url;
}
case SOURCE_TYPES.BANDCAMP: {
// Bandcamp doesn't have a simple embed URL transformation
// The full page URL works in an iframe for playback
return url;
}
default:
return url;
}

View File

@ -42,7 +42,11 @@ const EMBED_SOURCE_TYPES: readonly string[] = [
SOURCE_TYPES.SOUNDCLOUD,
SOURCE_TYPES.SPOTIFY,
SOURCE_TYPES.DAILYMOTION,
SOURCE_TYPES.TWITCH
SOURCE_TYPES.TWITCH,
SOURCE_TYPES.APPLE_MUSIC,
SOURCE_TYPES.DEEZER,
SOURCE_TYPES.TIDAL,
SOURCE_TYPES.BANDCAMP
];
/**

View File

@ -29,7 +29,11 @@ const EMBEDDED_CONTROL_TYPES: readonly string[] = [
SOURCE_TYPES.SOUNDCLOUD,
SOURCE_TYPES.SPOTIFY,
SOURCE_TYPES.DAILYMOTION,
SOURCE_TYPES.TWITCH
SOURCE_TYPES.TWITCH,
SOURCE_TYPES.APPLE_MUSIC,
SOURCE_TYPES.DEEZER,
SOURCE_TYPES.TIDAL,
SOURCE_TYPES.BANDCAMP
];
interface IProps {

View File

@ -36,6 +36,10 @@ export const SOURCE_TYPES = {
SPOTIFY: 'spotify',
DAILYMOTION: 'dailymotion',
TWITCH: 'twitch',
APPLE_MUSIC: 'apple_music',
DEEZER: 'deezer',
TIDAL: 'tidal',
BANDCAMP: 'bandcamp',
DIRECT: 'direct'
} as const;
@ -73,3 +77,23 @@ export const DAILYMOTION_URL_DOMAIN = 'dailymotion.com';
* Twitch domain.
*/
export const TWITCH_URL_DOMAIN = 'twitch.tv';
/**
* Apple Music domain.
*/
export const APPLE_MUSIC_URL_DOMAIN = 'music.apple.com';
/**
* Deezer domain.
*/
export const DEEZER_URL_DOMAIN = 'deezer.com';
/**
* Tidal domain.
*/
export const TIDAL_URL_DOMAIN = 'tidal.com';
/**
* Bandcamp domain.
*/
export const BANDCAMP_URL_DOMAIN = 'bandcamp.com';

View File

@ -3,13 +3,17 @@ import { IJitsiConference } from '../base/conference/reducer';
import { toState } from '../base/redux/functions';
import {
APPLE_MUSIC_URL_DOMAIN,
BANDCAMP_URL_DOMAIN,
DAILYMOTION_URL_DOMAIN,
DEEZER_URL_DOMAIN,
PLAYBACK_START,
PLAYBACK_STATUSES,
SHARED_MUSIC,
SOUNDCLOUD_URL_DOMAIN,
SOURCE_TYPES,
SPOTIFY_URL_DOMAIN,
TIDAL_URL_DOMAIN,
TWITCH_URL_DOMAIN,
VIMEO_URL_DOMAIN,
YOUTUBE_MUSIC_URL_DOMAIN,
@ -115,6 +119,64 @@ function getSpotifyInfo(url: string): { id: string; type: string; } | null {
return result ? { type: result[1], id: result[2] } : null;
}
/**
* Extracts Apple Music info from URL.
*
* @param {string} url - The entered URL.
* @returns {Object|null} The Apple Music info if matched.
*/
function getAppleMusicInfo(url: string): { id: string; path: string; } | null {
if (!url) {
return null;
}
// Matches music.apple.com/{country}/album/{name}/{id} or /playlist/{name}/{id}
const p = /(?:https?:\/\/)?music\.apple\.com\/([a-z]{2})\/(album|playlist|song)\/([^/]+)\/([a-zA-Z0-9.]+)/;
const result = url.match(p);
if (result) {
return { path: `${result[1]}/${result[2]}/${result[3]}/${result[4]}`, id: result[4] };
}
return null;
}
/**
* Extracts Deezer info from URL.
*
* @param {string} url - The entered URL.
* @returns {Object|null} The Deezer info if matched.
*/
function getDeezerInfo(url: string): { id: string; type: string; } | null {
if (!url) {
return null;
}
// Matches deezer.com/{country}/track/123, /album/123, /playlist/123
const p = /(?:https?:\/\/)?(?:www\.)?deezer\.com\/(?:[a-z]{2}\/)?(track|album|playlist)\/(\d+)/;
const result = url.match(p);
return result ? { type: result[1], id: result[2] } : null;
}
/**
* Extracts Tidal info from URL.
*
* @param {string} url - The entered URL.
* @returns {Object|null} The Tidal info if matched.
*/
function getTidalInfo(url: string): { id: string; type: string; } | null {
if (!url) {
return null;
}
// Matches tidal.com/browse/track/123, /album/123, /playlist/{uuid}
const p = /(?:https?:\/\/)?(?:www\.)?(?:listen\.)?tidal\.com\/(?:browse\/)?(track|album|playlist|mix)\/([a-zA-Z0-9-]+)/;
const result = url.match(p);
return result ? { type: result[1], id: result[2] } : null;
}
/**
* Checks if the status is one that is actually sharing music - playing, pause or start.
*
@ -180,6 +242,22 @@ export function getSourceType(url: string): SourceType {
if (hostname.includes(TWITCH_URL_DOMAIN)) {
return SOURCE_TYPES.TWITCH;
}
if (hostname.includes(APPLE_MUSIC_URL_DOMAIN)) {
return SOURCE_TYPES.APPLE_MUSIC;
}
if (hostname.includes(DEEZER_URL_DOMAIN)) {
return SOURCE_TYPES.DEEZER;
}
if (hostname.includes(TIDAL_URL_DOMAIN)) {
return SOURCE_TYPES.TIDAL;
}
if (hostname.includes(BANDCAMP_URL_DOMAIN)) {
return SOURCE_TYPES.BANDCAMP;
}
} catch (_) {
// Not a valid URL
}
@ -276,6 +354,47 @@ export function extractMusicUrl(input: string): {
};
}
// Apple Music
const appleMusicInfo = getAppleMusicInfo(trimmedLink);
if (appleMusicInfo) {
return {
url: trimmedLink,
sourceType: SOURCE_TYPES.APPLE_MUSIC,
embedInfo: { id: appleMusicInfo.id, type: appleMusicInfo.path }
};
}
// Deezer
const deezerInfo = getDeezerInfo(trimmedLink);
if (deezerInfo) {
return {
url: trimmedLink,
sourceType: SOURCE_TYPES.DEEZER,
embedInfo: deezerInfo
};
}
// Tidal
const tidalInfo = getTidalInfo(trimmedLink);
if (tidalInfo) {
return {
url: trimmedLink,
sourceType: SOURCE_TYPES.TIDAL,
embedInfo: tidalInfo
};
}
// Bandcamp - use full URL for embedding
if (hostname.includes(BANDCAMP_URL_DOMAIN)) {
return {
url: trimmedLink,
sourceType: SOURCE_TYPES.BANDCAMP
};
}
// It's a direct URL (audio/video file)
return {
url: trimmedLink,

View File

@ -34,6 +34,17 @@ export const PLAYBACK_STATUSES = {
*/
export const PLAYBACK_START = 'start';
/**
* Video source types.
*/
export const VIDEO_SOURCE_TYPES = {
YOUTUBE: 'youtube',
VIMEO: 'vimeo',
DAILYMOTION: 'dailymotion',
TWITCH: 'twitch',
DIRECT: 'direct'
} as const;
/**
* The domain for youtube URLs.
*/
@ -46,5 +57,6 @@ export const ALLOW_ALL_URL_DOMAINS = '*';
/**
* The default white listed domains for shared video.
* Allow all domains so Vimeo, Dailymotion, Twitch etc. work out of the box.
*/
export const DEFAULT_ALLOWED_URL_DOMAINS = [ YOUTUBE_URL_DOMAIN ];
export const DEFAULT_ALLOWED_URL_DOMAINS = [ ALLOW_ALL_URL_DOMAINS ];

View File

@ -9,6 +9,7 @@ import {
PLAYBACK_STATUSES,
SHARED_VIDEO,
VIDEO_PLAYER_PARTICIPANT_NAME,
VIDEO_SOURCE_TYPES,
YOUTUBE_PLAYER_PARTICIPANT_NAME,
YOUTUBE_URL_DOMAIN
} from './constants';
@ -139,6 +140,95 @@ export function isURLAllowedForSharedVideo(url: string,
return false;
}
/**
* Determines the video source type from a URL or YouTube ID.
*
* @param {string} url - The video URL or YouTube ID.
* @returns {string} The source type.
*/
export function getVideoSourceType(url: string): string {
if (!url) {
return VIDEO_SOURCE_TYPES.DIRECT;
}
// If it doesn't look like a URL, it's a YouTube ID
if (!url.match(/^https?:\/\//)) {
return VIDEO_SOURCE_TYPES.YOUTUBE;
}
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname.toLowerCase();
if (hostname.includes('youtube.com') || hostname.includes('youtu.be')) {
return VIDEO_SOURCE_TYPES.YOUTUBE;
}
if (hostname.includes('vimeo.com')) {
return VIDEO_SOURCE_TYPES.VIMEO;
}
if (hostname.includes('dailymotion.com') || hostname.includes('dai.ly')) {
return VIDEO_SOURCE_TYPES.DAILYMOTION;
}
if (hostname.includes('twitch.tv')) {
return VIDEO_SOURCE_TYPES.TWITCH;
}
} catch (_) {
// Not a valid URL
}
return VIDEO_SOURCE_TYPES.DIRECT;
}
/**
* Generates an embed URL for video platforms.
*
* @param {string} url - The original URL.
* @param {string} sourceType - The source type.
* @returns {string} The embed URL.
*/
export function getVideoEmbedUrl(url: string, sourceType: string): string {
switch (sourceType) {
case VIDEO_SOURCE_TYPES.VIMEO: {
const vimeoMatch = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
if (vimeoMatch) {
return `https://player.vimeo.com/video/${vimeoMatch[1]}?autoplay=1&autopause=0`;
}
return url;
}
case VIDEO_SOURCE_TYPES.DAILYMOTION: {
const dmMatch = url.match(/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/);
if (dmMatch) {
return `https://www.dailymotion.com/embed/video/${dmMatch[1]}?autoplay=1`;
}
return url;
}
case VIDEO_SOURCE_TYPES.TWITCH: {
const parent = window.location.hostname;
const videoMatch = url.match(/twitch\.tv\/videos\/(\d+)/);
const channelMatch = url.match(/twitch\.tv\/([a-zA-Z0-9_]+)(?:\?|$)/);
if (videoMatch) {
return `https://player.twitch.tv/?video=v${videoMatch[1]}&parent=${parent}&autoplay=true`;
}
if (channelMatch && channelMatch[1] !== 'videos') {
return `https://player.twitch.tv/?channel=${channelMatch[1]}&parent=${parent}&autoplay=true`;
}
return url;
}
default:
return url;
}
}
/**
* Sends SHARED_VIDEO command.
*

View File

@ -56,7 +56,7 @@ export const THRESHOLDS = [
},
{
width: 200,
order: [ 'microphone', 'camera' ]
order: [ 'microphone', 'camera', 'hangup' ]
}
];
@ -162,6 +162,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'invite',
'linktosalesforce',
'livestreaming',
'meetingintelligence',
'microphone',
'mute-everyone',
'mute-video-everyone',

View File

@ -17,9 +17,11 @@
color: #fff;
font-size: 16px;
font-weight: 500;
padding: 12px 24px;
padding: 14px 28px;
text-decoration: none;
transition: opacity 0.2s;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.meeting-intelligence-link a:hover {
opacity: 0.9;