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 {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 15;
|
||||
top: 15px;
|
||||
width: $watermarkWidth;
|
||||
height: $watermarkHeight;
|
||||
background-size: contain;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@
|
|||
|
||||
.toolbox-content-wrapper::after {
|
||||
content: '';
|
||||
display: block;
|
||||
background: $newToolbarBackgroundColor;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<DialogErrorBoundary>
|
||||
<DialogContainer />
|
||||
</DialogErrorBoundary>
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue