355 lines
9.2 KiB
TypeScript
355 lines
9.2 KiB
TypeScript
import { IStateful } from '../base/app/types';
|
|
import { IJitsiConference } from '../base/conference/reducer';
|
|
import { toState } from '../base/redux/functions';
|
|
|
|
import {
|
|
DAILYMOTION_URL_DOMAIN,
|
|
PLAYBACK_START,
|
|
PLAYBACK_STATUSES,
|
|
SHARED_MUSIC,
|
|
SOUNDCLOUD_URL_DOMAIN,
|
|
SOURCE_TYPES,
|
|
SPOTIFY_URL_DOMAIN,
|
|
TWITCH_URL_DOMAIN,
|
|
VIMEO_URL_DOMAIN,
|
|
YOUTUBE_MUSIC_URL_DOMAIN,
|
|
YOUTUBE_URL_DOMAIN
|
|
} from './constants';
|
|
import { SourceType } from './types';
|
|
|
|
/**
|
|
* Validates the entered URL and extracts YouTube ID if applicable.
|
|
*
|
|
* @param {string} url - The entered URL.
|
|
* @returns {string | null} The youtube video id if matched.
|
|
*/
|
|
function getYoutubeId(url: string): string | null {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
// eslint-disable-next-line max-len
|
|
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?(?:music\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
|
|
const result = url.match(p);
|
|
|
|
return result ? result[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extracts Vimeo video ID from URL.
|
|
*
|
|
* @param {string} url - The entered URL.
|
|
* @returns {string | null} The Vimeo video id if matched.
|
|
*/
|
|
function getVimeoId(url: string): string | null {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
// Matches vimeo.com/123456789 or player.vimeo.com/video/123456789
|
|
const p = /(?:https?:\/\/)?(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d+)/;
|
|
const result = url.match(p);
|
|
|
|
return result ? result[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extracts Dailymotion video ID from URL.
|
|
*
|
|
* @param {string} url - The entered URL.
|
|
* @returns {string | null} The Dailymotion video id if matched.
|
|
*/
|
|
function getDailymotionId(url: string): string | null {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
// Matches dailymotion.com/video/x123abc or dai.ly/x123abc
|
|
const p = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
|
|
const result = url.match(p);
|
|
|
|
return result ? result[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Extracts Twitch channel or video from URL.
|
|
*
|
|
* @param {string} url - The entered URL.
|
|
* @returns {Object|null} The Twitch info if matched.
|
|
*/
|
|
function getTwitchInfo(url: string): { id: string; type: string; } | null {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
// Matches twitch.tv/channel or twitch.tv/videos/123456
|
|
const channelMatch = url.match(/(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([a-zA-Z0-9_]+)(?:\?|$)/);
|
|
const videoMatch = url.match(/(?:https?:\/\/)?(?:www\.)?twitch\.tv\/videos\/(\d+)/);
|
|
|
|
if (videoMatch) {
|
|
return { type: 'video', id: videoMatch[1] };
|
|
}
|
|
|
|
if (channelMatch && channelMatch[1] !== 'videos') {
|
|
return { type: 'channel', id: channelMatch[1] };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Extracts Spotify track/album/playlist from URL.
|
|
*
|
|
* @param {string} url - The entered URL.
|
|
* @returns {Object|null} The Spotify info if matched.
|
|
*/
|
|
function getSpotifyInfo(url: string): { id: string; type: string; } | null {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
// Matches open.spotify.com/track/123, /album/123, /playlist/123
|
|
const p = /(?:https?:\/\/)?open\.spotify\.com\/(track|album|playlist|episode|show)\/([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.
|
|
*
|
|
* @param {string} status - The shared music status.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isSharingStatus(status: string): boolean {
|
|
return [ PLAYBACK_STATUSES.PLAYING, PLAYBACK_STATUSES.PAUSED, PLAYBACK_START ].includes(status);
|
|
}
|
|
|
|
/**
|
|
* Determines the source type of the given URL.
|
|
*
|
|
* @param {string} url - The URL to check.
|
|
* @returns {SourceType} The source type.
|
|
*/
|
|
export function getSourceType(url: string): SourceType {
|
|
if (getYoutubeId(url)) {
|
|
return SOURCE_TYPES.YOUTUBE;
|
|
}
|
|
|
|
if (getVimeoId(url)) {
|
|
return SOURCE_TYPES.VIMEO;
|
|
}
|
|
|
|
if (getDailymotionId(url)) {
|
|
return SOURCE_TYPES.DAILYMOTION;
|
|
}
|
|
|
|
if (getTwitchInfo(url)) {
|
|
return SOURCE_TYPES.TWITCH;
|
|
}
|
|
|
|
if (getSpotifyInfo(url)) {
|
|
return SOURCE_TYPES.SPOTIFY;
|
|
}
|
|
|
|
try {
|
|
const urlObj = new URL(url);
|
|
const hostname = urlObj.hostname.toLowerCase();
|
|
|
|
if (hostname.includes(YOUTUBE_URL_DOMAIN)
|
|
|| hostname.includes(YOUTUBE_MUSIC_URL_DOMAIN)) {
|
|
return SOURCE_TYPES.YOUTUBE;
|
|
}
|
|
|
|
if (hostname.includes(VIMEO_URL_DOMAIN)) {
|
|
return SOURCE_TYPES.VIMEO;
|
|
}
|
|
|
|
if (hostname.includes(SOUNDCLOUD_URL_DOMAIN)) {
|
|
return SOURCE_TYPES.SOUNDCLOUD;
|
|
}
|
|
|
|
if (hostname.includes(SPOTIFY_URL_DOMAIN)) {
|
|
return SOURCE_TYPES.SPOTIFY;
|
|
}
|
|
|
|
if (hostname.includes(DAILYMOTION_URL_DOMAIN)) {
|
|
return SOURCE_TYPES.DAILYMOTION;
|
|
}
|
|
|
|
if (hostname.includes(TWITCH_URL_DOMAIN)) {
|
|
return SOURCE_TYPES.TWITCH;
|
|
}
|
|
} catch (_) {
|
|
// Not a valid URL
|
|
}
|
|
|
|
return SOURCE_TYPES.DIRECT;
|
|
}
|
|
|
|
/**
|
|
* Extracts media info from URL based on the source type.
|
|
*
|
|
* @param {string} input - The user input.
|
|
* @returns {Object | undefined} An object with url, sourceType, and optional embedInfo.
|
|
*/
|
|
export function extractMusicUrl(input: string): {
|
|
embedInfo?: { id: string; type?: string; };
|
|
sourceType: SourceType;
|
|
url: string;
|
|
} | undefined {
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
const trimmedLink = input.trim();
|
|
|
|
if (!trimmedLink) {
|
|
return;
|
|
}
|
|
|
|
// YouTube
|
|
const youtubeId = getYoutubeId(trimmedLink);
|
|
|
|
if (youtubeId) {
|
|
return {
|
|
url: youtubeId,
|
|
sourceType: SOURCE_TYPES.YOUTUBE,
|
|
embedInfo: { id: youtubeId }
|
|
};
|
|
}
|
|
|
|
// Vimeo
|
|
const vimeoId = getVimeoId(trimmedLink);
|
|
|
|
if (vimeoId) {
|
|
return {
|
|
url: trimmedLink,
|
|
sourceType: SOURCE_TYPES.VIMEO,
|
|
embedInfo: { id: vimeoId }
|
|
};
|
|
}
|
|
|
|
// Dailymotion
|
|
const dailymotionId = getDailymotionId(trimmedLink);
|
|
|
|
if (dailymotionId) {
|
|
return {
|
|
url: trimmedLink,
|
|
sourceType: SOURCE_TYPES.DAILYMOTION,
|
|
embedInfo: { id: dailymotionId }
|
|
};
|
|
}
|
|
|
|
// Twitch
|
|
const twitchInfo = getTwitchInfo(trimmedLink);
|
|
|
|
if (twitchInfo) {
|
|
return {
|
|
url: trimmedLink,
|
|
sourceType: SOURCE_TYPES.TWITCH,
|
|
embedInfo: twitchInfo
|
|
};
|
|
}
|
|
|
|
// Spotify
|
|
const spotifyInfo = getSpotifyInfo(trimmedLink);
|
|
|
|
if (spotifyInfo) {
|
|
return {
|
|
url: trimmedLink,
|
|
sourceType: SOURCE_TYPES.SPOTIFY,
|
|
embedInfo: spotifyInfo
|
|
};
|
|
}
|
|
|
|
// Check if the URL is valid for other sources
|
|
try {
|
|
const urlObj = new URL(trimmedLink);
|
|
const hostname = urlObj.hostname.toLowerCase();
|
|
|
|
// SoundCloud - we use the full URL for embedding
|
|
if (hostname.includes(SOUNDCLOUD_URL_DOMAIN)) {
|
|
return {
|
|
url: trimmedLink,
|
|
sourceType: SOURCE_TYPES.SOUNDCLOUD
|
|
};
|
|
}
|
|
|
|
// It's a direct URL (audio/video file)
|
|
return {
|
|
url: trimmedLink,
|
|
sourceType: SOURCE_TYPES.DIRECT
|
|
};
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if shared music functionality is enabled.
|
|
*
|
|
* @param {IStateful} stateful - The redux store or getState function.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isSharedMusicEnabled(stateful: IStateful): boolean {
|
|
const state = toState(stateful);
|
|
const { disableThirdPartyRequests = false } = state['features/base/config'];
|
|
|
|
return !disableThirdPartyRequests;
|
|
}
|
|
|
|
/**
|
|
* Returns true if music is currently playing.
|
|
*
|
|
* @param {IStateful} stateful - The redux store or getState function.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isMusicPlaying(stateful: IStateful): boolean {
|
|
const state = toState(stateful);
|
|
const { status } = state['features/shared-music'];
|
|
|
|
return isSharingStatus(status ?? '');
|
|
}
|
|
|
|
/**
|
|
* Sends SHARED_MUSIC command.
|
|
*
|
|
* @param {Object} options - The command options.
|
|
* @param {string} options.id - The id of the music.
|
|
* @param {string} options.status - The status of the shared music.
|
|
* @param {IJitsiConference} options.conference - The current conference.
|
|
* @param {string} options.localParticipantId - The id of the local participant.
|
|
* @param {number} options.time - The seek position of the music.
|
|
* @param {boolean} options.muted - Whether the music is muted.
|
|
* @param {number} options.volume - The volume level.
|
|
* @param {string} options.sourceType - The source type.
|
|
* @param {string} options.title - The track title.
|
|
* @returns {void}
|
|
*/
|
|
export function sendShareMusicCommand({ id, status, conference, localParticipantId = '', time, muted, volume,
|
|
sourceType, title }: {
|
|
conference?: IJitsiConference;
|
|
id: string;
|
|
localParticipantId?: string;
|
|
muted?: boolean;
|
|
sourceType?: string;
|
|
status: string;
|
|
time: number;
|
|
title?: string;
|
|
volume?: number;
|
|
}): void {
|
|
conference?.sendCommandOnce(SHARED_MUSIC, {
|
|
value: id,
|
|
attributes: {
|
|
from: localParticipantId,
|
|
muted,
|
|
sourceType,
|
|
state: status,
|
|
time,
|
|
title,
|
|
volume
|
|
}
|
|
});
|
|
}
|