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:
parent
1706dc47a3
commit
8dc9c74c45
|
|
@ -106,7 +106,7 @@ form {
|
||||||
.watermark {
|
.watermark {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 15;
|
top: 15px;
|
||||||
width: $watermarkWidth;
|
width: $watermarkWidth;
|
||||||
height: $watermarkHeight;
|
height: $watermarkHeight;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(28, 32, 37, 0.98);
|
background: rgba(28, 32, 37, 0.98);
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
z-index: 350;
|
z-index: 350;
|
||||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-intelligence-header {
|
.meeting-intelligence-header {
|
||||||
|
|
@ -42,8 +44,8 @@
|
||||||
border: none;
|
border: none;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@
|
||||||
|
|
||||||
.toolbox-content-wrapper::after {
|
.toolbox-content-wrapper::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
display: block;
|
||||||
background: $newToolbarBackgroundColor;
|
background: $newToolbarBackgroundColor;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-indicator {
|
.layout-indicator {
|
||||||
bottom: 80px;
|
bottom: calc(100px + env(safe-area-inset-bottom, 0px));
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -37,13 +37,13 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 2;
|
z-index: 251;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-indicator-dot {
|
.layout-indicator-dot {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
width: 6px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-indicator-dot--active {
|
.layout-indicator-dot--active {
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
#localVideoWrapper video,
|
#localVideoWrapper video,
|
||||||
#localVideoWrapper object {
|
#localVideoWrapper object {
|
||||||
border-radius: $borderRadius !important;
|
border-radius: $borderRadius !important;
|
||||||
cursor: hand;
|
cursor: pointer;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: hand;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > video {
|
& > video {
|
||||||
cursor: hand;
|
cursor: pointer;
|
||||||
border-radius: $borderRadius;
|
border-radius: $borderRadius;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
<!--#include virtual="head.html" -->
|
<!--#include virtual="head.html" -->
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="content-type" content="text/html;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="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="theme-color" content="#2A3A4B">
|
<meta name="theme-color" content="#1e1040">
|
||||||
<!--#include virtual="base.html" -->
|
<!--#include virtual="base.html" -->
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"dompurify": "3.2.6",
|
"dompurify": "3.2.6",
|
||||||
"dropbox": "10.7.0",
|
"dropbox": "10.7.0",
|
||||||
|
"emoji-mart": "^5.6.0",
|
||||||
"focus-visible": "5.1.0",
|
"focus-visible": "5.1.0",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"grapheme-splitter": "1.0.4",
|
"grapheme-splitter": "1.0.4",
|
||||||
|
|
@ -132,6 +133,7 @@
|
||||||
"@babel/preset-env": "7.25.9",
|
"@babel/preset-env": "7.25.9",
|
||||||
"@babel/preset-react": "7.25.9",
|
"@babel/preset-react": "7.25.9",
|
||||||
"@jitsi/eslint-config": "6.0.4",
|
"@jitsi/eslint-config": "6.0.4",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@react-native-community/cli": "15.0.1",
|
"@react-native-community/cli": "15.0.1",
|
||||||
"@react-native-community/cli-platform-android": "15.0.1",
|
"@react-native-community/cli-platform-android": "15.0.1",
|
||||||
"@react-native-community/cli-platform-ios": "15.0.1",
|
"@react-native-community/cli-platform-ios": "15.0.1",
|
||||||
|
|
@ -5247,6 +5249,22 @@
|
||||||
"url": "https://opencollective.com/unts"
|
"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": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.21",
|
"version": "1.0.0-next.21",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
||||||
|
|
@ -13088,6 +13106,12 @@
|
||||||
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==",
|
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
|
@ -21198,6 +21222,53 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/pngjs": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||||
|
|
@ -30480,6 +30551,15 @@
|
||||||
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
|
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
|
||||||
"dev": true
|
"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": {
|
"@polka/url": {
|
||||||
"version": "1.0.0-next.21",
|
"version": "1.0.0-next.21",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz",
|
||||||
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ=="
|
"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": {
|
"emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
|
@ -41782,6 +41867,31 @@
|
||||||
"find-up": "^4.0.0"
|
"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": {
|
"pngjs": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"dompurify": "3.2.6",
|
"dompurify": "3.2.6",
|
||||||
"dropbox": "10.7.0",
|
"dropbox": "10.7.0",
|
||||||
|
"emoji-mart": "^5.6.0",
|
||||||
"focus-visible": "5.1.0",
|
"focus-visible": "5.1.0",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"grapheme-splitter": "1.0.4",
|
"grapheme-splitter": "1.0.4",
|
||||||
|
|
@ -138,6 +139,7 @@
|
||||||
"@babel/preset-env": "7.25.9",
|
"@babel/preset-env": "7.25.9",
|
||||||
"@babel/preset-react": "7.25.9",
|
"@babel/preset-react": "7.25.9",
|
||||||
"@jitsi/eslint-config": "6.0.4",
|
"@jitsi/eslint-config": "6.0.4",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@react-native-community/cli": "15.0.1",
|
"@react-native-community/cli": "15.0.1",
|
||||||
"@react-native-community/cli-platform-android": "15.0.1",
|
"@react-native-community/cli-platform-android": "15.0.1",
|
||||||
"@react-native-community/cli-platform-ios": "15.0.1",
|
"@react-native-community/cli-platform-ios": "15.0.1",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import GlobalStyles from '../../base/ui/components/GlobalStyles.web';
|
import GlobalStyles from '../../base/ui/components/GlobalStyles.web';
|
||||||
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web';
|
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web';
|
||||||
import DialogContainer from '../../base/ui/components/web/DialogContainer';
|
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 ChromeExtensionBanner from '../../chrome-extension-banner/components/ChromeExtensionBanner.web';
|
||||||
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
|
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
|
||||||
import PiP from '../../pip/components/PiP';
|
import PiP from '../../pip/components/PiP';
|
||||||
|
|
@ -62,7 +63,9 @@ export class App extends AbstractApp {
|
||||||
override _renderDialogContainer() {
|
override _renderDialogContainer() {
|
||||||
return (
|
return (
|
||||||
<JitsiThemeProvider>
|
<JitsiThemeProvider>
|
||||||
<DialogContainer />
|
<DialogErrorBoundary>
|
||||||
|
<DialogContainer />
|
||||||
|
</DialogErrorBoundary>
|
||||||
</JitsiThemeProvider>
|
</JitsiThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,35 @@ function getEmbedUrl(url: string, sourceType: string): string {
|
||||||
|
|
||||||
return url;
|
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:
|
default:
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,11 @@ const EMBED_SOURCE_TYPES: readonly string[] = [
|
||||||
SOURCE_TYPES.SOUNDCLOUD,
|
SOURCE_TYPES.SOUNDCLOUD,
|
||||||
SOURCE_TYPES.SPOTIFY,
|
SOURCE_TYPES.SPOTIFY,
|
||||||
SOURCE_TYPES.DAILYMOTION,
|
SOURCE_TYPES.DAILYMOTION,
|
||||||
SOURCE_TYPES.TWITCH
|
SOURCE_TYPES.TWITCH,
|
||||||
|
SOURCE_TYPES.APPLE_MUSIC,
|
||||||
|
SOURCE_TYPES.DEEZER,
|
||||||
|
SOURCE_TYPES.TIDAL,
|
||||||
|
SOURCE_TYPES.BANDCAMP
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,11 @@ const EMBEDDED_CONTROL_TYPES: readonly string[] = [
|
||||||
SOURCE_TYPES.SOUNDCLOUD,
|
SOURCE_TYPES.SOUNDCLOUD,
|
||||||
SOURCE_TYPES.SPOTIFY,
|
SOURCE_TYPES.SPOTIFY,
|
||||||
SOURCE_TYPES.DAILYMOTION,
|
SOURCE_TYPES.DAILYMOTION,
|
||||||
SOURCE_TYPES.TWITCH
|
SOURCE_TYPES.TWITCH,
|
||||||
|
SOURCE_TYPES.APPLE_MUSIC,
|
||||||
|
SOURCE_TYPES.DEEZER,
|
||||||
|
SOURCE_TYPES.TIDAL,
|
||||||
|
SOURCE_TYPES.BANDCAMP
|
||||||
];
|
];
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ export const SOURCE_TYPES = {
|
||||||
SPOTIFY: 'spotify',
|
SPOTIFY: 'spotify',
|
||||||
DAILYMOTION: 'dailymotion',
|
DAILYMOTION: 'dailymotion',
|
||||||
TWITCH: 'twitch',
|
TWITCH: 'twitch',
|
||||||
|
APPLE_MUSIC: 'apple_music',
|
||||||
|
DEEZER: 'deezer',
|
||||||
|
TIDAL: 'tidal',
|
||||||
|
BANDCAMP: 'bandcamp',
|
||||||
DIRECT: 'direct'
|
DIRECT: 'direct'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -73,3 +77,23 @@ export const DAILYMOTION_URL_DOMAIN = 'dailymotion.com';
|
||||||
* Twitch domain.
|
* Twitch domain.
|
||||||
*/
|
*/
|
||||||
export const TWITCH_URL_DOMAIN = 'twitch.tv';
|
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';
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ import { IJitsiConference } from '../base/conference/reducer';
|
||||||
import { toState } from '../base/redux/functions';
|
import { toState } from '../base/redux/functions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
APPLE_MUSIC_URL_DOMAIN,
|
||||||
|
BANDCAMP_URL_DOMAIN,
|
||||||
DAILYMOTION_URL_DOMAIN,
|
DAILYMOTION_URL_DOMAIN,
|
||||||
|
DEEZER_URL_DOMAIN,
|
||||||
PLAYBACK_START,
|
PLAYBACK_START,
|
||||||
PLAYBACK_STATUSES,
|
PLAYBACK_STATUSES,
|
||||||
SHARED_MUSIC,
|
SHARED_MUSIC,
|
||||||
SOUNDCLOUD_URL_DOMAIN,
|
SOUNDCLOUD_URL_DOMAIN,
|
||||||
SOURCE_TYPES,
|
SOURCE_TYPES,
|
||||||
SPOTIFY_URL_DOMAIN,
|
SPOTIFY_URL_DOMAIN,
|
||||||
|
TIDAL_URL_DOMAIN,
|
||||||
TWITCH_URL_DOMAIN,
|
TWITCH_URL_DOMAIN,
|
||||||
VIMEO_URL_DOMAIN,
|
VIMEO_URL_DOMAIN,
|
||||||
YOUTUBE_MUSIC_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;
|
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.
|
* 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)) {
|
if (hostname.includes(TWITCH_URL_DOMAIN)) {
|
||||||
return SOURCE_TYPES.TWITCH;
|
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 (_) {
|
} catch (_) {
|
||||||
// Not a valid URL
|
// 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)
|
// It's a direct URL (audio/video file)
|
||||||
return {
|
return {
|
||||||
url: trimmedLink,
|
url: trimmedLink,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,17 @@ export const PLAYBACK_STATUSES = {
|
||||||
*/
|
*/
|
||||||
export const PLAYBACK_START = 'start';
|
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.
|
* The domain for youtube URLs.
|
||||||
*/
|
*/
|
||||||
|
|
@ -46,5 +57,6 @@ export const ALLOW_ALL_URL_DOMAINS = '*';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default white listed domains for shared video.
|
* 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 ];
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
PLAYBACK_STATUSES,
|
PLAYBACK_STATUSES,
|
||||||
SHARED_VIDEO,
|
SHARED_VIDEO,
|
||||||
VIDEO_PLAYER_PARTICIPANT_NAME,
|
VIDEO_PLAYER_PARTICIPANT_NAME,
|
||||||
|
VIDEO_SOURCE_TYPES,
|
||||||
YOUTUBE_PLAYER_PARTICIPANT_NAME,
|
YOUTUBE_PLAYER_PARTICIPANT_NAME,
|
||||||
YOUTUBE_URL_DOMAIN
|
YOUTUBE_URL_DOMAIN
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
@ -139,6 +140,95 @@ export function isURLAllowedForSharedVideo(url: string,
|
||||||
return false;
|
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.
|
* Sends SHARED_VIDEO command.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export const THRESHOLDS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
width: 200,
|
width: 200,
|
||||||
order: [ 'microphone', 'camera' ]
|
order: [ 'microphone', 'camera', 'hangup' ]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -162,6 +162,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
|
||||||
'invite',
|
'invite',
|
||||||
'linktosalesforce',
|
'linktosalesforce',
|
||||||
'livestreaming',
|
'livestreaming',
|
||||||
|
'meetingintelligence',
|
||||||
'microphone',
|
'microphone',
|
||||||
'mute-everyone',
|
'mute-everyone',
|
||||||
'mute-video-everyone',
|
'mute-video-everyone',
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 12px 24px;
|
padding: 14px 28px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
touch-action: manipulation;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.meeting-intelligence-link a:hover {
|
.meeting-intelligence-link a:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue