@aigamo/nostalgic-diva
Version:
React function components for imperatively controlling embedded players (audio, Niconico, SoundCloud and YouTube) using refs.
1 lines • 74.9 kB
Source Map (JSON)
{"version":3,"file":"index-vLjOJb_8.cjs","sources":["../src/controllers/Logger.ts","../src/controllers/PlayerController.ts","../src/services/VideoService.ts","../src/services/AudioVideoService.ts","../src/services/DailymotionVideoService.ts","../src/services/NiconicoVideoService.ts","../src/services/SoundCloudVideoService.ts","../src/services/TwitchVideoService.ts","../src/services/VimeoVideoService.ts","../src/services/YouTubeVideoService.ts","../src/services/findVideoService.ts","../src/controllers/NullPlayerController.ts","../src/components/NostalgicDivaProvider.tsx","../src/components/NostalgicDiva.tsx","../src/components/defineNostalgicDiva.tsx","../src/controllers/PlayerControllerImpl.ts","../src/controllers/AudioPlayerController.ts","../src/controllers/DailymotionPlayerController.ts","../src/controllers/NiconicoPlayerController.ts","../src/controllers/SoundCloudPlayerController.ts","../src/controllers/TwitchPlayerController.ts","../src/controllers/VimeoPlayerController.ts","../src/controllers/YouTubePlayerController.ts"],"sourcesContent":["// https://source.dot.net/#Microsoft.Extensions.Logging.Abstractions/LogLevel.cs,d07793e8b722b77e,references\nexport enum LogLevel {\n\t/**\n\t * Logs that contain the most detailed messages. These messages may contain sensitive application data.\n\t * These messages are disabled by default and should never be enabled in a production environment.\n\t */\n\tTrace = 0,\n\t/**\n\t * Logs that are used for interactive investigation during development. These logs should primarily contain\n\t * information useful for debugging and have no long-term value.\n\t */\n\tDebug = 1,\n\t/**\n\t * Logs that track the general flow of the application. These logs should have long-term value.\n\t */\n\tInformation = 2,\n\t/**\n\t * Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the\n\t * application execution to stop.\n\t */\n\tWarning = 3,\n\t/**\n\t * Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a\n\t * failure in the current activity, not an application-wide failure.\n\t */\n\tError = 4,\n\t/**\n\t * Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires\n\t * immediate attention.\n\t */\n\tCritical = 5,\n\t/**\n\t * Not used for writing log messages. Specifies that a logging category should not write any messages.\n\t */\n\tNone = 6,\n}\n\n// https://source.dot.net/#Microsoft.Extensions.Logging.Abstractions/ILogger.cs,0976525f5d1b9e54,references\nexport interface ILogger {\n\tisEnabled(logLevel: LogLevel): boolean;\n\tlog(logLevel: LogLevel, message?: any, ...optionalParams: any[]): void;\n}\n\nexport class Logger implements ILogger {\n\tprivate readonly title = 'nostalgic-diva';\n\n\tprivate createMessage(message: any): string {\n\t\treturn `[${this.title}] ${message}`;\n\t}\n\n\tprivate debug(message?: any, ...optionalParams: any): void {\n\t\tconsole.debug(this.createMessage(message), ...optionalParams);\n\t}\n\n\tprivate error(message?: any, ...optionalParams: any): void {\n\t\tconsole.error(this.createMessage(message), ...optionalParams);\n\t}\n\n\tprivate warn(message?: any, ...optionalParams: any): void {\n\t\tconsole.warn(this.createMessage(message), ...optionalParams);\n\t}\n\n\tisEnabled(): boolean {\n\t\treturn true;\n\t}\n\n\tlog(logLevel: LogLevel, message?: any, ...optionalParams: any[]): void {\n\t\tswitch (logLevel) {\n\t\t\tcase LogLevel.Debug:\n\t\t\t\tthis.debug(message, ...optionalParams);\n\t\t\t\tbreak;\n\t\t\tcase LogLevel.Warning:\n\t\t\t\tthis.warn(message, ...optionalParams);\n\t\t\t\tbreak;\n\t\t\tcase LogLevel.Error:\n\t\t\t\tthis.error(message, ...optionalParams);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n","import { ILogger, LogLevel } from './Logger';\nimport { PlayerControllerImpl } from './PlayerControllerImpl';\n\nexport enum PlayerType {\n\t'Audio' = 'Audio',\n\t'Dailymotion' = 'Dailymotion',\n\t'Niconico' = 'Niconico',\n\t'SoundCloud' = 'SoundCloud',\n\t'Twitch' = 'Twitch',\n\t'Vimeo' = 'Vimeo',\n\t'YouTube' = 'YouTube',\n}\n\nexport function validatePlayerType(value: string): value is PlayerType {\n\treturn PlayerType[value as keyof typeof PlayerType] !== undefined;\n}\n\nexport interface LoadedEvent {\n\tid: string;\n}\n\nexport interface TimeEvent {\n\tduration: number | undefined;\n\tpercent: number | undefined;\n\tseconds: number | undefined;\n}\n\nexport interface PlayerOptions {\n\tonError?(event: any): void;\n\tonLoaded?(event: LoadedEvent): void;\n\tonPlay?(): void;\n\tonPause?(): void;\n\tonEnded?(): void;\n\tonTimeUpdate?(event: TimeEvent): void;\n}\n\nexport interface IPlayerCommands {\n\tloadVideo(id: string): Promise<void>;\n\tplay(): Promise<void>;\n\tpause(): Promise<void>;\n\tsetCurrentTime(seconds: number): Promise<void>;\n\tsetVolume(volume: number): Promise<void>;\n\tsetMuted(muted: boolean): Promise<void>;\n\tsetPlaybackRate(playbackRate: number): Promise<void>;\n\tgetDuration(): Promise<number | undefined>;\n\tgetCurrentTime(): Promise<number | undefined>;\n\tgetVolume(): Promise<number | undefined>;\n\tgetPlaybackRate(): Promise<number | undefined>;\n}\n\nexport interface IPlayerController extends IPlayerCommands {\n\tsupports(command: keyof IPlayerCommands): boolean;\n}\n\nexport class PlayerController<\n\tTPlayer extends object,\n\tTController extends PlayerControllerImpl<TPlayer>,\n> implements IPlayerController\n{\n\tprivate static nextId = 1;\n\n\tprivate readonly id: number;\n\tprivate impl?: TController;\n\n\tconstructor(\n\t\tprivate readonly logger: ILogger,\n\t\tprivate readonly type: PlayerType,\n\t\tprivate readonly player: TPlayer,\n\t\tprivate readonly options: PlayerOptions | undefined,\n\t\tprivate readonly controllerFactory: new (\n\t\t\tlogger: ILogger,\n\t\t\tplayer: TPlayer,\n\t\t\toptions: PlayerOptions | undefined,\n\t\t) => TController,\n\t) {\n\t\tthis.id = PlayerController.nextId++;\n\t}\n\n\tprivate createMessage(message: any): string {\n\t\treturn `${this.type}#${this.id} ${message}`;\n\t}\n\n\tpublic debug(message?: any, ...optionalParams: any): void {\n\t\tthis.logger.log(\n\t\t\tLogLevel.Debug,\n\t\t\tthis.createMessage(message),\n\t\t\t...optionalParams,\n\t\t);\n\t}\n\n\tpublic error(message?: any, ...optionalParams: any): void {\n\t\tthis.logger.log(\n\t\t\tLogLevel.Error,\n\t\t\tthis.createMessage(message),\n\t\t\t...optionalParams,\n\t\t);\n\t}\n\n\tasync attach(id: string): Promise<void> {\n\t\tthis.debug('attach', id);\n\n\t\tif (this.impl) {\n\t\t\tthis.debug('player is already attached');\n\t\t\treturn;\n\t\t}\n\n\t\tthis.debug('Attaching player...');\n\n\t\tthis.impl = new this.controllerFactory(\n\t\t\tthis.logger,\n\t\t\tthis.player,\n\t\t\tthis.options,\n\t\t);\n\n\t\tawait this.impl.attach(id);\n\n\t\tthis.debug('player attached');\n\t}\n\n\tprivate createPlayerNotAttachedError(): Error {\n\t\treturn new Error('player is not attached');\n\t}\n\n\tprivate createCommandNotSupportedError(\n\t\tcommand: keyof IPlayerCommands,\n\t): Error {\n\t\treturn new Error(`${command} is not supported`);\n\t}\n\n\tasync detach(): Promise<void> {\n\t\tthis.debug('detach');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tawait this.impl.detach();\n\n\t\tthis.impl = undefined;\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tthis.debug('loadVideo', id);\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.loadVideo === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('loadVideo');\n\t\t}\n\n\t\tthis.debug('Loading video...');\n\n\t\tawait this.impl.loadVideo(id);\n\n\t\tthis.debug('video loaded', id);\n\t}\n\n\tplay(): Promise<void> {\n\t\tthis.debug('play');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.play === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('play');\n\t\t}\n\n\t\treturn this.impl.play();\n\t}\n\n\tpause(): Promise<void> {\n\t\tthis.debug('pause');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.pause === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('pause');\n\t\t}\n\n\t\treturn this.impl.pause();\n\t}\n\n\tsetCurrentTime(seconds: number): Promise<void> {\n\t\tthis.debug('setCurrentTime', seconds);\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.setCurrentTime === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('setCurrentTime');\n\t\t}\n\n\t\treturn this.impl.setCurrentTime(seconds);\n\t}\n\n\tsetVolume(volume: number): Promise<void> {\n\t\tthis.debug('setVolume', volume);\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.setVolume === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('setVolume');\n\t\t}\n\n\t\treturn this.impl.setVolume(volume);\n\t}\n\n\tsetMuted(muted: boolean): Promise<void> {\n\t\tthis.debug('setMuted', muted);\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.setMuted === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('setMuted');\n\t\t}\n\n\t\treturn this.impl.setMuted(muted);\n\t}\n\n\tsetPlaybackRate(playbackRate: number): Promise<void> {\n\t\tthis.debug('setPlaybackRate', playbackRate);\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.setPlaybackRate === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('setPlaybackRate');\n\t\t}\n\n\t\treturn this.impl.setPlaybackRate(playbackRate);\n\t}\n\n\tgetDuration(): Promise<number | undefined> {\n\t\tthis.debug('getDuration');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.getDuration === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('getDuration');\n\t\t}\n\n\t\treturn this.impl.getDuration();\n\t}\n\n\tgetCurrentTime(): Promise<number | undefined> {\n\t\tthis.debug('getCurrentTime');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.getCurrentTime === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('getCurrentTime');\n\t\t}\n\n\t\treturn this.impl.getCurrentTime();\n\t}\n\n\tgetVolume(): Promise<number | undefined> {\n\t\tthis.debug('getVolume');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.getVolume === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('getVolume');\n\t\t}\n\n\t\treturn this.impl.getVolume();\n\t}\n\n\tgetPlaybackRate(): Promise<number | undefined> {\n\t\tthis.debug('getPlaybackRate');\n\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\tif (this.impl.getPlaybackRate === undefined) {\n\t\t\tthrow this.createCommandNotSupportedError('getPlaybackRate');\n\t\t}\n\n\t\treturn this.impl.getPlaybackRate();\n\t}\n\n\tsupports(command: keyof IPlayerCommands): boolean {\n\t\tif (this.impl === undefined) {\n\t\t\tthrow this.createPlayerNotAttachedError();\n\t\t}\n\n\t\treturn this.impl.supports(command);\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\n\nexport abstract class VideoService<TPlayerType extends PlayerType> {\n\tprotected constructor(readonly type: TPlayerType) {}\n\n\tabstract canPlay(url: string): boolean;\n\n\tabstract extractVideoId(url: string): string | undefined;\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\n// https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/patterns.js\nconst AUDIO_EXTENSIONS =\n\t/\\.(m4a|m4b|mp4a|mpga|mp2|mp2a|mp3|m2a|m3a|wav|weba|aac|oga|spx)($|\\?)/i;\nconst VIDEO_EXTENSIONS = /\\.(mp4|og[gv]|webm|mov|m4v)(#t=[,\\d+]+)?($|\\?)/i;\n\nexport class AudioVideoService extends VideoService<PlayerType.Audio> {\n\tconstructor() {\n\t\tsuper(PlayerType.Audio);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn AUDIO_EXTENSIONS.test(url) || VIDEO_EXTENSIONS.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\treturn url;\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\n// https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/patterns.js\nconst MATCH_URL_DAILYMOTION =\n\t/^(?:(?:https?):)?(?:\\/\\/)?(?:www\\.)?(?:(?:dailymotion\\.com(?:\\/embed)?\\/video)|dai\\.ly)\\/([a-zA-Z0-9]+)(?:_[\\w_-]+)?(?:[\\w.#_-]+)?/;\n\nexport class DailymotionVideoService extends VideoService<PlayerType.Dailymotion> {\n\tconstructor() {\n\t\tsuper(PlayerType.Dailymotion);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn MATCH_URL_DAILYMOTION.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\tconst matches = MATCH_URL_DAILYMOTION.exec(url);\n\t\treturn matches?.[1];\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\nconst MATCH_URL_NICONICO = /(?:www\\.|)?nicovideo\\.jp\\/watch\\/(\\w+)$/;\n\nexport class NiconicoVideoService extends VideoService<PlayerType.Niconico> {\n\tconstructor() {\n\t\tsuper(PlayerType.Niconico);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn MATCH_URL_NICONICO.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\tconst matches = MATCH_URL_NICONICO.exec(url);\n\t\treturn matches?.[1];\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\n// https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/patterns.js\nconst MATCH_URL_SOUNDCLOUD = /(?:soundcloud\\.com|snd\\.sc)\\/[^.]+$/;\n\nexport class SoundCloudVideoService extends VideoService<PlayerType.SoundCloud> {\n\tconstructor() {\n\t\tsuper(PlayerType.SoundCloud);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn MATCH_URL_SOUNDCLOUD.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\treturn url;\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\n// https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/patterns.js\nconst MATCH_URL_TWITCH_VIDEO = /(?:www\\.|go\\.)?twitch\\.tv\\/videos\\/(\\d+)($|\\?)/;\n\nexport class TwitchVideoService extends VideoService<PlayerType.Twitch> {\n\tconstructor() {\n\t\tsuper(PlayerType.Twitch);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn MATCH_URL_TWITCH_VIDEO.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\tconst matches = MATCH_URL_TWITCH_VIDEO.exec(url);\n\t\treturn matches?.[1];\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\n// https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/patterns.js\nconst MATCH_URL_VIMEO = /vimeo\\.com\\/(\\d+)$/;\n\nexport class VimeoVideoService extends VideoService<PlayerType.Vimeo> {\n\tconstructor() {\n\t\tsuper(PlayerType.Vimeo);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn MATCH_URL_VIMEO.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\treturn MATCH_URL_VIMEO.exec(url)?.[1];\n\t}\n}\n","import { PlayerType } from '../controllers/PlayerController';\nimport { VideoService } from './VideoService';\n\n// https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/patterns.js\nconst MATCH_URL_YOUTUBE =\n\t/(?:youtu\\.be\\/|youtube(?:-nocookie|education)?\\.com\\/(?:embed\\/|v\\/|watch\\/|watch\\?v=|watch\\?.+&v=|shorts\\/|live\\/))((\\w|-){11})|youtube\\.com\\/playlist\\?list=|youtube\\.com\\/user\\//;\n\nexport class YouTubeVideoService extends VideoService<PlayerType.YouTube> {\n\tconstructor() {\n\t\tsuper(PlayerType.YouTube);\n\t}\n\n\tcanPlay(url: string): boolean {\n\t\treturn MATCH_URL_YOUTUBE.test(url);\n\t}\n\n\textractVideoId(url: string): string | undefined {\n\t\tconst matches = MATCH_URL_YOUTUBE.exec(url);\n\t\treturn matches?.[1];\n\t}\n}\n","import { AudioVideoService } from './AudioVideoService';\nimport { DailymotionVideoService } from './DailymotionVideoService';\nimport { NiconicoVideoService } from './NiconicoVideoService';\nimport { SoundCloudVideoService } from './SoundCloudVideoService';\nimport { TwitchVideoService } from './TwitchVideoService';\nimport { VimeoVideoService } from './VimeoVideoService';\nimport { YouTubeVideoService } from './YouTubeVideoService';\n\nconst videoServices = [\n\tnew AudioVideoService(),\n\tnew DailymotionVideoService(),\n\tnew NiconicoVideoService(),\n\tnew SoundCloudVideoService(),\n\tnew TwitchVideoService(),\n\tnew VimeoVideoService(),\n\tnew YouTubeVideoService(),\n] as const;\n\nexport function findVideoService(\n\turl: string,\n): (typeof videoServices)[number] | undefined {\n\treturn videoServices.find((videoService) => videoService.canPlay(url));\n}\n","import {\n\tIPlayerCommands,\n\tIPlayerController,\n} from '@/controllers/PlayerController';\n\nclass NullPlayerController implements IPlayerController {\n\tasync attach(): Promise<void> {}\n\n\tasync detach(): Promise<void> {}\n\n\tasync loadVideo(id: string): Promise<void> {}\n\n\tasync play(): Promise<void> {}\n\n\tasync pause(): Promise<void> {}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {}\n\n\tasync setVolume(volume: number): Promise<void> {}\n\n\tasync setMuted(muted: boolean): Promise<void> {}\n\n\tasync setPlaybackRate(playbackRate: number): Promise<void> {}\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn undefined;\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn undefined;\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn undefined;\n\t}\n\n\tasync getPlaybackRate(): Promise<number | undefined> {\n\t\treturn undefined;\n\t}\n\n\tsupports(command: keyof IPlayerCommands): boolean {\n\t\treturn false;\n\t}\n}\n\nexport const nullPlayerController = new NullPlayerController();\n","import React, {\n\tReactElement,\n\tReactNode,\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseMemo,\n\tuseRef,\n} from 'react';\n\nimport { IPlayerCommands, IPlayerController } from '../controllers';\nimport { nullPlayerController } from '@/controllers/NullPlayerController';\n\ninterface NostalgicDivaContextProps extends IPlayerController {\n\thandleControllerChange: (value: IPlayerController) => void;\n}\n\nconst NostalgicDivaContext = createContext<NostalgicDivaContextProps>(\n\t// eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n\tundefined!,\n);\n\ninterface NostalgicDivaProviderProps {\n\tchildren?: ReactNode;\n}\n\nexport const NostalgicDivaProvider = ({\n\tchildren,\n}: NostalgicDivaProviderProps): ReactElement => {\n\tconst controllerRef = useRef<IPlayerController>(nullPlayerController);\n\n\tconst handleControllerChange = useCallback(\n\t\t(value: IPlayerController): void => {\n\t\t\tcontrollerRef.current = value;\n\t\t},\n\t\t[],\n\t);\n\n\tconst loadVideo = useCallback(async (id: string): Promise<void> => {\n\t\tawait controllerRef.current.loadVideo(id);\n\t}, []);\n\n\tconst play = useCallback(async (): Promise<void> => {\n\t\tawait controllerRef.current.play();\n\t}, []);\n\n\tconst pause = useCallback(async (): Promise<void> => {\n\t\tawait controllerRef.current.pause();\n\t}, []);\n\n\tconst setCurrentTime = useCallback(\n\t\tasync (seconds: number): Promise<void> => {\n\t\t\tawait controllerRef.current.setCurrentTime(seconds);\n\t\t\tawait controllerRef.current.play();\n\t\t},\n\t\t[],\n\t);\n\n\tconst setVolume = useCallback(async (volume: number): Promise<void> => {\n\t\tawait controllerRef.current.setVolume(volume);\n\t}, []);\n\n\tconst setMuted = useCallback(async (muted: boolean): Promise<void> => {\n\t\tawait controllerRef.current.setMuted(muted);\n\t}, []);\n\n\tconst setPlaybackRate = useCallback(\n\t\tasync (playbackRate: number): Promise<void> => {\n\t\t\tawait controllerRef.current.setPlaybackRate(playbackRate);\n\t\t},\n\t\t[],\n\t);\n\n\tconst getDuration = useCallback(async (): Promise<number | undefined> => {\n\t\treturn await controllerRef.current.getDuration();\n\t}, []);\n\n\tconst getCurrentTime = useCallback(async (): Promise<\n\t\tnumber | undefined\n\t> => {\n\t\treturn await controllerRef.current.getCurrentTime();\n\t}, []);\n\n\tconst getVolume = useCallback(async (): Promise<number | undefined> => {\n\t\treturn await controllerRef.current.getVolume();\n\t}, []);\n\n\tconst getPlaybackRate = useCallback(async (): Promise<\n\t\tnumber | undefined\n\t> => {\n\t\treturn await controllerRef.current.getPlaybackRate();\n\t}, []);\n\n\tconst supports = useCallback(\n\t\t(command: keyof IPlayerCommands): boolean =>\n\t\t\tcontrollerRef.current.supports(command),\n\t\t[],\n\t);\n\n\tconst value = useMemo(\n\t\t(): NostalgicDivaContextProps => ({\n\t\t\thandleControllerChange,\n\t\t\tloadVideo,\n\t\t\tplay,\n\t\t\tpause,\n\t\t\tsetCurrentTime,\n\t\t\tsetVolume,\n\t\t\tsetMuted,\n\t\t\tsetPlaybackRate,\n\t\t\tgetDuration,\n\t\t\tgetCurrentTime,\n\t\t\tgetVolume,\n\t\t\tgetPlaybackRate,\n\t\t\tsupports,\n\t\t}),\n\t\t[\n\t\t\thandleControllerChange,\n\t\t\tloadVideo,\n\t\t\tplay,\n\t\t\tpause,\n\t\t\tsetCurrentTime,\n\t\t\tsetVolume,\n\t\t\tsetMuted,\n\t\t\tsetPlaybackRate,\n\t\t\tgetDuration,\n\t\t\tgetCurrentTime,\n\t\t\tgetVolume,\n\t\t\tgetPlaybackRate,\n\t\t\tsupports,\n\t\t],\n\t);\n\n\treturn (\n\t\t<NostalgicDivaContext.Provider value={value}>\n\t\t\t{children}\n\t\t</NostalgicDivaContext.Provider>\n\t);\n};\n\nexport const useNostalgicDiva = (): NostalgicDivaContextProps => {\n\treturn useContext(NostalgicDivaContext);\n};\n","import React, {\n\tElementType,\n\tReactElement,\n\tSuspense,\n\tlazy,\n\tmemo,\n\tuseCallback,\n} from 'react';\n\nimport { ILogger, LogLevel, Logger } from '../controllers/Logger';\nimport {\n\tIPlayerController,\n\tPlayerOptions,\n\tPlayerType,\n} from '../controllers/PlayerController';\nimport { findVideoService } from '../services/findVideoService';\nimport { useNostalgicDiva } from './NostalgicDivaProvider';\nimport { PlayerProps } from './PlayerContainer';\n\nconst players: Record<PlayerType, ElementType<PlayerProps>> = {\n\tAudio: lazy(() => import('./AudioPlayer')),\n\tDailymotion: lazy(() => import('./DailymotionPlayer')),\n\tNiconico: lazy(() => import('./NiconicoPlayer')),\n\tSoundCloud: lazy(() => import('./SoundCloudPlayer')),\n\tTwitch: lazy(() => import('./TwitchPlayer')),\n\tVimeo: lazy(() => import('./VimeoPlayer')),\n\tYouTube: lazy(() => import('./YouTubePlayer')),\n};\n\nexport interface NostalgicDivaProps {\n\tsrc: string;\n\toptions?: PlayerOptions;\n\tlogger?: ILogger;\n\tonControllerChange?: (value: IPlayerController) => void;\n}\n\nconst defaultLogger = new Logger();\n\nfunction getTypeAndVideoId(\n\turl: string,\n): { type: PlayerType; videoId: string } | undefined {\n\tconst videoService = findVideoService(url);\n\tif (videoService === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst { type, extractVideoId } = videoService;\n\n\tconst videoId = extractVideoId(url);\n\tif (videoId === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn { type: type, videoId: videoId };\n}\n\nexport const NostalgicDiva = memo(\n\t({\n\t\tsrc,\n\t\toptions,\n\t\tlogger = defaultLogger,\n\t\tonControllerChange,\n\t}: NostalgicDivaProps): ReactElement => {\n\t\t// useNostalgicDiva may return undefined if NostalgicDiva is used without NostalgicDivaProvider.\n\t\tconst diva = useNostalgicDiva() as\n\t\t\t| ReturnType<typeof useNostalgicDiva>\n\t\t\t| undefined;\n\n\t\tconst handleControllerChange = useCallback(\n\t\t\t(value: IPlayerController) =>\n\t\t\t\t(onControllerChange ?? diva?.handleControllerChange)?.(value),\n\t\t\t[diva?.handleControllerChange, onControllerChange],\n\t\t);\n\n\t\tlogger.log(LogLevel.Debug, 'NostalgicDiva');\n\n\t\tconst typeAndVideoId = getTypeAndVideoId(src);\n\t\tif (typeAndVideoId === undefined) {\n\t\t\treturn (\n\t\t\t\t<div style={{ width: '100%', height: '100%' }}>\n\t\t\t\t\t<iframe\n\t\t\t\t\t\tsrc=\"about:blank\"\n\t\t\t\t\t\ttitle=\"about:blank\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\theight: '100%',\n\t\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tconst { type, videoId } = typeAndVideoId;\n\n\t\tconst Player = players[type];\n\n\t\treturn (\n\t\t\t<Suspense fallback={null}>\n\t\t\t\t<Player\n\t\t\t\t\tlogger={logger}\n\t\t\t\t\ttype={type}\n\t\t\t\t\tonControllerChange={handleControllerChange}\n\t\t\t\t\tvideoId={videoId}\n\t\t\t\t\toptions={options}\n\t\t\t\t/>\n\t\t\t</Suspense>\n\t\t);\n\t},\n);\n","import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport {\n\tIPlayerCommands,\n\tIPlayerController,\n\tPlayerOptions,\n} from '../controllers/PlayerController';\nimport { NostalgicDiva } from './NostalgicDiva';\nimport { nullPlayerController } from '@/controllers/NullPlayerController';\n\nexport class NostalgicDivaElement\n\textends HTMLElement\n\timplements IPlayerController\n{\n\tstatic readonly observedAttributes = ['src'];\n\n\tcontainer: ShadowRoot;\n\tcontroller: IPlayerController = nullPlayerController;\n\n\tconstructor() {\n\t\tsuper();\n\n\t\tthis.container = this.attachShadow({ mode: 'closed' });\n\t}\n\n\tget src(): string {\n\t\treturn this.getAttribute('src') ?? '';\n\t}\n\n\tset src(value: string) {\n\t\tthis.setAttribute('src', value);\n\t}\n\n\treadonly #options: PlayerOptions = {\n\t\tonError: (e) =>\n\t\t\tthis.dispatchEvent(new CustomEvent('error', { detail: e })),\n\t\tonLoaded: (e) =>\n\t\t\tthis.dispatchEvent(new CustomEvent('loaded', { detail: e })),\n\t\tonPlay: () => this.dispatchEvent(new CustomEvent('play')),\n\t\tonPause: () => this.dispatchEvent(new CustomEvent('pause')),\n\t\tonEnded: () => this.dispatchEvent(new CustomEvent('ended')),\n\t\tonTimeUpdate: (e) =>\n\t\t\tthis.dispatchEvent(new CustomEvent('timeupdate', { detail: e })),\n\t};\n\n\t#handleControllerChange = (value: IPlayerController): void => {\n\t\tconsole.debug(\n\t\t\t'[@nostalgic-diva/web-components] handleControllerChange',\n\t\t);\n\n\t\tthis.controller = value;\n\t};\n\n\t#render(): void {\n\t\tReactDOM.render(\n\t\t\t<NostalgicDiva\n\t\t\t\tsrc={this.src}\n\t\t\t\toptions={this.#options}\n\t\t\t\tonControllerChange={this.#handleControllerChange}\n\t\t\t/>,\n\t\t\tthis.container,\n\t\t);\n\t}\n\n\tconnectedCallback(): void {\n\t\tconsole.debug('[@nostalgic-diva/web-components] connectedCallback');\n\n\t\tthis.#render();\n\t}\n\n\tdisconnectedCallback(): void {\n\t\tconsole.debug('[@nostalgic-diva/web-components] disconnectedCallback');\n\t}\n\n\tattributeChangedCallback(): void {\n\t\tconsole.debug(\n\t\t\t'[@nostalgic-diva/web-components] attributeChangedCallback',\n\t\t);\n\n\t\tthis.#render();\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tawait this.controller.loadVideo(id);\n\t}\n\n\tasync play(): Promise<void> {\n\t\tawait this.controller.play();\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tawait this.controller.pause();\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tawait this.controller.setCurrentTime(seconds);\n\t}\n\n\tasync setVolume(volume: number): Promise<void> {\n\t\tawait this.controller.setVolume(volume);\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tawait this.controller.setMuted(muted);\n\t}\n\n\tasync setPlaybackRate(playbackRate: number): Promise<void> {\n\t\tawait this.controller.setPlaybackRate(playbackRate);\n\t}\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn await this.controller.getDuration();\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn await this.controller.getCurrentTime();\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn await this.controller.getVolume();\n\t}\n\n\tasync getPlaybackRate(): Promise<number | undefined> {\n\t\treturn await this.controller.getPlaybackRate();\n\t}\n\n\tsupports(command: keyof IPlayerCommands): boolean {\n\t\treturn this.controller.supports(command);\n\t}\n}\n\nexport function defineNostalgicDiva(): void {\n\tcustomElements.define('nostalgic-diva', NostalgicDivaElement);\n}\n","import { ILogger, LogLevel } from './Logger';\nimport { IPlayerCommands, PlayerOptions } from './PlayerController';\n\nexport abstract class PlayerControllerImpl<TPlayer>\n\timplements Partial<IPlayerCommands>\n{\n\tconstructor(\n\t\tprotected readonly logger: ILogger,\n\t\tprotected readonly player: TPlayer,\n\t\tprotected readonly options: PlayerOptions | undefined,\n\t) {\n\t\tthis.logger.log(LogLevel.Debug, 'ctor');\n\t}\n\n\tabstract attach(id: string): Promise<void>;\n\tabstract detach(): Promise<void>;\n\tabstract loadVideo?(id: string): Promise<void>;\n\tabstract play?(): Promise<void>;\n\tabstract pause?(): Promise<void>;\n\tabstract setCurrentTime?(seconds: number): Promise<void>;\n\tabstract setVolume?(volume: number): Promise<void>;\n\tabstract setMuted?(muted: boolean): Promise<void>;\n\tabstract setPlaybackRate?(playbackRate: number): Promise<void>;\n\tabstract getDuration?(): Promise<number | undefined>;\n\tabstract getCurrentTime?(): Promise<number | undefined>;\n\tabstract getVolume?(): Promise<number | undefined>;\n\tabstract getPlaybackRate?(): Promise<number | undefined>;\n\n\tsupports(command: keyof IPlayerCommands): boolean {\n\t\treturn this[command] !== undefined;\n\t}\n}\n","import { PlayerControllerImpl } from './PlayerControllerImpl';\n\n// https://github.com/VocaDB/vocadb/blob/61b8c54f3eca906a477101dab4fdd9b154be310e/VocaDbWeb/Scripts/ViewModels/PVs/PVPlayerFile.ts.\nexport class AudioPlayerController extends PlayerControllerImpl<HTMLAudioElement> {\n\tasync attach(): Promise<void> {\n\t\tthis.player.onerror = (event): void => this.options?.onError?.(event);\n\t\tthis.player.onloadeddata = (): void =>\n\t\t\tthis.options?.onLoaded?.({ id: this.player.src });\n\t\tthis.player.onplay = (): void => this.options?.onPlay?.();\n\t\tthis.player.onpause = (): void => this.options?.onPause?.();\n\t\tthis.player.onended = (): void => this.options?.onEnded?.();\n\t\tthis.player.ontimeupdate = (): void => {\n\t\t\tthis.options?.onTimeUpdate?.({\n\t\t\t\tduration: this.player.duration,\n\t\t\t\tpercent: this.player.currentTime / this.player.duration,\n\t\t\t\tseconds: this.player.currentTime,\n\t\t\t});\n\t\t};\n\t}\n\n\tasync detach(): Promise<void> {\n\t\tthis.player.onerror = null;\n\t\tthis.player.onloadeddata = null;\n\t\tthis.player.onplay = null;\n\t\tthis.player.onpause = null;\n\t\tthis.player.onended = null;\n\t\tthis.player.ontimeupdate = null;\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tthis.player.src = id;\n\t}\n\n\tasync play(): Promise<void> {\n\t\tthis.player.play();\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tthis.player.pause();\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tthis.player.currentTime = seconds;\n\t}\n\n\tasync setVolume(volume: number): Promise<void> {\n\t\tthis.player.volume = volume;\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tthis.player.muted = muted;\n\t}\n\n\tasync setPlaybackRate(playbackRate: number): Promise<void> {\n\t\tthis.player.playbackRate = playbackRate;\n\t}\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn this.player.duration;\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn this.player.currentTime;\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn this.player.volume;\n\t}\n\n\tasync getPlaybackRate(): Promise<number | undefined> {\n\t\treturn this.player.playbackRate;\n\t}\n}\n","import { PlayerControllerImpl } from './PlayerControllerImpl';\n\nconst events = [\n\t'apiready',\n\t'seeked',\n\t'video_end',\n\t'durationchange',\n\t'pause',\n\t'playing',\n\t'error',\n] satisfies DM.EventType[];\n\nexport class DailymotionPlayerController extends PlayerControllerImpl<DM.player> {\n\tprivate handlePlayerEvents = (e: { type: DM.EventType }): void => {\n\t\tswitch (e.type) {\n\t\t\tcase 'apiready':\n\t\t\t\tthis.options?.onLoaded?.({ id: this.player.video.videoId });\n\t\t\t\tbreak;\n\t\t\tcase 'seeked':\n\t\t\t\tthis.options?.onTimeUpdate?.({\n\t\t\t\t\tduration: this.player.duration,\n\t\t\t\t\tpercent: this.player.currentTime / this.player.duration,\n\t\t\t\t\tseconds: this.player.currentTime,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase 'video_end':\n\t\t\t\tthis.options?.onEnded?.();\n\t\t\t\tbreak;\n\t\t\tcase 'durationchange':\n\t\t\t\tbreak;\n\t\t\tcase 'pause':\n\t\t\t\tthis.options?.onPause?.();\n\t\t\t\tbreak;\n\t\t\tcase 'playing':\n\t\t\t\tthis.options?.onPlay?.();\n\t\t\t\tbreak;\n\t\t\tcase 'error':\n\t\t\t\tthis.options?.onError?.(e);\n\t\t\t\tbreak;\n\t\t}\n\t};\n\n\tasync attach(id: string): Promise<void> {\n\t\tfor (const event of events) {\n\t\t\tthis.player.addEventListener(event, this.handlePlayerEvents);\n\t\t}\n\t}\n\n\tasync detach(): Promise<void> {\n\t\tfor (const event of events) {\n\t\t\tthis.player.removeEventListener(event, this.handlePlayerEvents);\n\t\t}\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tthis.player.load(id);\n\t}\n\n\tasync play(): Promise<void> {\n\t\tthis.player.play();\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tthis.player.pause();\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tthis.player.seek(seconds);\n\t}\n\n\tasync setVolume(volume: number): Promise<void> {\n\t\tthis.player.setVolume(volume);\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tthis.player.setMuted(muted);\n\t}\n\n\tsetPlaybackRate = undefined;\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn this.player.duration;\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn this.player.currentTime;\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn this.player.volume;\n\t}\n\n\tgetPlaybackRate = undefined;\n}\n","import { LogLevel } from './Logger';\nimport { PlayerControllerImpl } from './PlayerControllerImpl';\n\ndeclare global {\n\tinterface Window {\n\t\tonNicoPlayerFactoryReady: (callback: nico.NicoPlayerFactory) => void;\n\t}\n}\n\nenum PlayerStatus {\n\tPlay = 2,\n\tPause = 3,\n\tEnd = 4,\n}\n\n// https://github.com/VocaDB/vocadb/blob/a4b5f9d8186772d7e6f58f997bbcbb51509d2539/VocaDbWeb/Scripts/ViewModels/PVs/PVPlayerNico.ts.\nexport class NiconicoPlayerController extends PlayerControllerImpl<HTMLIFrameElement> {\n\tprivate static readonly origin = 'https://embed.nicovideo.jp';\n\n\tprivate duration?: number;\n\tprivate currentTime?: number;\n\tprivate volume?: number;\n\n\tprivate handleMessage = (e: nico.PlayerEvent): void => {\n\t\tif (e.origin !== NiconicoPlayerController.origin) return;\n\n\t\tconst data = e.data;\n\n\t\tswitch (data.eventName) {\n\t\t\tcase 'playerStatusChange':\n\t\t\t\tthis.logger.log(\n\t\t\t\t\tLogLevel.Debug,\n\t\t\t\t\t`player status changed: ${\n\t\t\t\t\t\tPlayerStatus[data.data.playerStatus] ??\n\t\t\t\t\t\tdata.data.playerStatus\n\t\t\t\t\t}`,\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'statusChange':\n\t\t\t\tthis.logger.log(\n\t\t\t\t\tLogLevel.Debug,\n\t\t\t\t\t`status changed: ${\n\t\t\t\t\t\tPlayerStatus[data.data.playerStatus] ??\n\t\t\t\t\t\tdata.data.playerStatus\n\t\t\t\t\t}`,\n\t\t\t\t);\n\n\t\t\t\tswitch (data.data.playerStatus) {\n\t\t\t\t\tcase PlayerStatus.Play:\n\t\t\t\t\t\tthis.options?.onPlay?.();\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase PlayerStatus.Pause:\n\t\t\t\t\t\tthis.options?.onPause?.();\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase PlayerStatus.End:\n\t\t\t\t\t\tthis.options?.onEnded?.();\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'playerMetadataChange':\n\t\t\t\tif (data.data.duration !== undefined)\n\t\t\t\t\tthis.duration = data.data.duration / 1000;\n\n\t\t\t\tthis.currentTime =\n\t\t\t\t\tdata.data.currentTime === undefined\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: data.data.currentTime / 1000;\n\n\t\t\t\tthis.volume = data.data.volume;\n\n\t\t\t\tthis.options?.onTimeUpdate?.({\n\t\t\t\t\tduration: this.duration,\n\t\t\t\t\tpercent:\n\t\t\t\t\t\tthis.currentTime !== undefined &&\n\t\t\t\t\t\tthis.duration !== undefined\n\t\t\t\t\t\t\t? this.currentTime / this.duration\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tseconds: this.currentTime,\n\t\t\t\t});\n\t\t\t\tbreak;\n\n\t\t\tcase 'loadComplete':\n\t\t\t\tthis.logger.log(LogLevel.Debug, 'load completed');\n\n\t\t\t\tthis.duration = data.data.videoInfo.lengthInSeconds;\n\n\t\t\t\tthis.options?.onLoaded?.({ id: data.data.videoInfo.watchId });\n\t\t\t\tbreak;\n\n\t\t\tcase 'error':\n\t\t\t\t// TODO: Implement.\n\n\t\t\t\tthis.options?.onError?.(data);\n\t\t\t\tbreak;\n\n\t\t\tcase 'player-error:video:play':\n\t\t\tcase 'player-error:video:seek':\n\t\t\t\tthis.options?.onError?.(data);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tthis.logger.log(\n\t\t\t\t\tLogLevel.Debug,\n\t\t\t\t\t'message',\n\t\t\t\t\t(data as any).eventName,\n\t\t\t\t\t(data as any).data,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\t};\n\n\tasync attach(): Promise<void> {\n\t\twindow.addEventListener('message', this.handleMessage);\n\t}\n\n\tasync detach(): Promise<void> {\n\t\twindow.removeEventListener('message', this.handleMessage);\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\treturn new Promise((resolve, reject /* TODO: Reject. */) => {\n\t\t\tthis.duration = undefined;\n\t\t\tthis.currentTime = undefined;\n\n\t\t\t// Wait for iframe to load.\n\t\t\tthis.player.onload = (): void => {\n\t\t\t\tthis.player.onload = null;\n\t\t\t\tresolve();\n\t\t\t};\n\n\t\t\tthis.player.src = `https://embed.nicovideo.jp/watch/${id}?jsapi=1&playerId=1`;\n\t\t});\n\t}\n\n\t// https://blog.hayu.io/web/create/nicovideo-embed-player-api/.\n\tprivate postMessage(message: any): void {\n\t\tthis.player.contentWindow?.postMessage(\n\t\t\t{\n\t\t\t\t...message,\n\t\t\t\tplayerId: '1' /* Needs to be a string, not a number. */,\n\t\t\t\tsourceConnectorType: 1,\n\t\t\t},\n\t\t\tNiconicoPlayerController.origin,\n\t\t);\n\t}\n\n\tasync play(): Promise<void> {\n\t\tthis.postMessage({ eventName: 'play' });\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tthis.postMessage({ eventName: 'pause' });\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tthis.postMessage({ eventName: 'seek', data: { time: seconds * 1000 } });\n\t}\n\n\tasync setVolume(volume: number): Promise<void> {\n\t\tthis.postMessage({\n\t\t\teventName: 'volumeChange',\n\t\t\tdata: { volume: volume },\n\t\t});\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tthis.postMessage({\n\t\t\teventName: 'mute',\n\t\t\tdata: { mute: muted },\n\t\t});\n\t}\n\n\tsetPlaybackRate = undefined;\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn this.duration;\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn this.currentTime;\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn this.volume;\n\t}\n\n\tgetPlaybackRate = undefined;\n}\n","import { PlayerControllerImpl } from './PlayerControllerImpl';\n\n// https://github.com/VocaDB/vocadb/blob/e147650a8f1f85c8fa865d0ab562126c278527ec/VocaDbWeb/Scripts/ViewModels/PVs/PVPlayerSoundCloud.ts.\nexport class SoundCloudPlayerController extends PlayerControllerImpl<SC.SoundCloudWidget> {\n\tprivate getDurationCore(): Promise<number> {\n\t\treturn new Promise((resolve, reject /* TODO: Reject. */) => {\n\t\t\tthis.player.getDuration(resolve);\n\t\t});\n\t}\n\n\tattach(id: string): Promise<void> {\n\t\treturn new Promise((resolve, reject /* TODO: reject */) => {\n\t\t\tthis.player.bind(SC.Widget.Events.READY, () => {\n\t\t\t\tthis.player.bind(\n\t\t\t\t\tSC.Widget.Events.PLAY_PROGRESS,\n\t\t\t\t\tasync (event) => {\n\t\t\t\t\t\tconst duration = await this.getDurationCore();\n\n\t\t\t\t\t\tthis.options?.onTimeUpdate?.({\n\t\t\t\t\t\t\tduration: duration / 1000,\n\t\t\t\t\t\t\tpercent: event.currentPosition / duration,\n\t\t\t\t\t\t\tseconds: event.currentPosition / 1000,\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tthis.player.bind(SC.Widget.Events.ERROR, (event) =>\n\t\t\t\t\tthis.options?.onError?.(event),\n\t\t\t\t);\n\t\t\t\tthis.player.bind(SC.Widget.Events.PLAY, () =>\n\t\t\t\t\tthis.options?.onPlay?.(),\n\t\t\t\t);\n\t\t\t\tthis.player.bind(SC.Widget.Events.PAUSE, () =>\n\t\t\t\t\tthis.options?.onPause?.(),\n\t\t\t\t);\n\t\t\t\tthis.player.bind(SC.Widget.Events.FINISH, () =>\n\t\t\t\t\tthis.options?.onEnded?.(),\n\t\t\t\t);\n\n\t\t\t\tthis.options?.onLoaded?.({ id: id });\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t}\n\n\tasync detach(): Promise<void> {\n\t\tthis.player.unbind(SC.Widget.Events.READY);\n\t\tthis.player.unbind(SC.Widget.Events.PLAY_PROGRESS);\n\t\tthis.player.unbind(SC.Widget.Events.ERROR);\n\t\tthis.player.unbind(SC.Widget.Events.PLAY);\n\t\tthis.player.unbind(SC.Widget.Events.PAUSE);\n\t\tthis.player.unbind(SC.Widget.Events.FINISH);\n\t}\n\n\tprivate static playerLoadAsync(\n\t\tplayer: SC.SoundCloudWidget,\n\t\turl: string,\n\t\toptions: Omit<SC.SoundCloudLoadOptions, 'callback'>,\n\t): Promise<void> {\n\t\treturn new Promise((resolve, reject /* TODO: Reject. */) => {\n\t\t\tplayer.load(url, { ...options, callback: resolve });\n\t\t});\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tawait SoundCloudPlayerController.playerLoadAsync(this.player, id, {\n\t\t\tauto_play: true,\n\t\t});\n\n\t\tthis.options?.onLoaded?.({ id: id });\n\t}\n\n\tasync play(): Promise<void> {\n\t\tthis.player.play();\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tthis.player.pause();\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tthis.player.seekTo(seconds * 1000);\n\t}\n\n\tasync setVolume(volume: number): Promise<void> {\n\t\tthis.player.setVolume(volume * 100);\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tthis.setVolume(muted ? 0 : 1 /* TODO */);\n\t}\n\n\tsetPlaybackRate = undefined;\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\tconst duration = await this.getDurationCore();\n\t\treturn duration / 1000;\n\t}\n\n\tprivate getCurrentTimeCore(): Promise<number> {\n\t\treturn new Promise((resolve, reject /* TODO: Reject. */) => {\n\t\t\tthis.player.getPosition(resolve);\n\t\t});\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\tconst position = await this.getCurrentTimeCore();\n\t\treturn position / 1000;\n\t}\n\n\tprivate getVolumeCore(): Promise<number> {\n\t\treturn new Promise((resolve, reject /* TODO: Reject. */) => {\n\t\t\tthis.player.getVolume(resolve);\n\t\t});\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\tconst volume = await this.getVolumeCore();\n\t\treturn volume / 100;\n\t}\n\n\tgetPlaybackRate = undefined;\n}\n","import { PlayerControllerImpl } from './PlayerControllerImpl';\n\nexport class TwitchPlayerController extends PlayerControllerImpl<Twitch.Player> {\n\tprivate handleReady = (): void => {\n\t\tthis.options?.onLoaded?.({ id: this.player.getVideo() });\n\t};\n\n\tprivate handlePlay = (): void => {\n\t\tthis.options?.onPlay?.();\n\t};\n\n\tprivate handlePause = (): void => {\n\t\tthis.options?.onPause?.();\n\t};\n\n\tprivate handleEnded = (): void => {\n\t\tthis.options?.onEnded?.();\n\t};\n\n\tprivate handleSeek = (): void => {\n\t\tthis.options?.onTimeUpdate?.({\n\t\t\tduration: 0,\n\t\t\tpercent: 0,\n\t\t\tseconds: 0,\n\t\t});\n\t};\n\n\tasync attach(id: string): Promise<void> {\n\t\tthis.player.addEventListener(Twitch.Player.READY, this.handleReady);\n\t\tthis.player.addEventListener(Twitch.Player.PLAYING, this.handlePlay);\n\t\tthis.player.addEventListener(Twitch.Player.PAUSE, this.handlePause);\n\t\tthis.player.addEventListener(Twitch.Player.ENDED, this.handleEnded);\n\t\tthis.player.addEventListener(Twitch.Player.SEEK, this.handleSeek);\n\t}\n\n\tasync detach(): Promise<void> {}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tthis.player.setVideo(id, 0);\n\t}\n\n\tasync play(): Promise<void> {\n\t\tthis.player.play();\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tthis.player.pause();\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tthis.player.seek(seconds);\n\t}\n\n\tasync setVolume(volume: number): Promise<void> {\n\t\tthis.player.setVolume(volume);\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tthis.player.setMuted(muted);\n\t}\n\n\tsetPlaybackRate = undefined;\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn this.player.getDuration();\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn this.player.getCurrentTime();\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn this.player.getVolume();\n\t}\n\n\tgetPlaybackRate = undefined;\n}\n","import { PlayerControllerImpl } from './PlayerControllerImpl';\n\n// https://github.com/cookpete/react-player/blob/e3c324bc6845698179d065fa408db515c2296b4b/src/players/Vimeo.js\nexport class VimeoPlayerController extends PlayerControllerImpl<Vimeo.Player> {\n\tasync attach(): Promise<void> {\n\t\tawait this.player.ready();\n\n\t\tthis.player.on('error', (data) => this.options?.onError?.(data));\n\t\tthis.player.on('loaded', (event) =>\n\t\t\tthis.options?.onLoaded?.({ id: event.id.toString() }),\n\t\t);\n\t\tthis.player.on('play', () => this.options?.onPlay?.());\n\t\tthis.player.on('pause', () => this.options?.onPause?.());\n\t\tthis.player.on('ended', () => this.options?.onEnded?.());\n\t\tthis.player.on('timeupdate', (data) => {\n\t\t\tthis.options?.onTimeUpdate?.({\n\t\t\t\tduration: data.duration,\n\t\t\t\tpercent: data.percent,\n\t\t\t\tseconds: data.seconds,\n\t\t\t});\n\t\t});\n\t}\n\n\tasync detach(): Promise<void> {\n\t\tthis.player.off('error');\n\t\tthis.player.off('loaded');\n\t\tthis.player.off('play');\n\t\tthis.player.off('pause');\n\t\tthis.player.off('ended');\n\t\tthis.player.off('timeupdate');\n\t}\n\n\tasync loadVideo(id: string): Promise<void> {\n\t\tawait this.player.loadVideo(id);\n\t}\n\n\tasync play(): Promise<void> {\n\t\tawait this.player.play();\n\t}\n\n\tasync pause(): Promise<void> {\n\t\tawait this.player.pause();\n\t}\n\n\tasync setCurrentTime(seconds: number): Promise<void> {\n\t\tawait this.player.setCurrentTime(seconds);\n\t}\n\n\tasync setVolume(fraction: number): Promise<void> {\n\t\tawait this.player.setVolume(fraction);\n\t}\n\n\tasync setMuted(muted: boolean): Promise<void> {\n\t\tawait this.player.setMuted(muted);\n\t}\n\n\tasync setPlaybackRate(playbackRate: number): Promise<void> {\n\t\tawait this.player.setPlaybackRate(playbackRate);\n\t}\n\n\tasync getDuration(): Promise<number | undefined> {\n\t\treturn this.player.getDuration();\n\t}\n\n\tasync getCurrentTime(): Promise<number | undefined> {\n\t\treturn this.player.getCurrentTime();\n\t}\n\n\tasync getVolume(): Promise<number | undefined> {\n\t\treturn this.player.getVolume();\n\t}\n\n\tasync getPlaybackRate(): Promise<number | undefined> {\n\t\treturn this.player.getPlaybackRate();\n\t}\n}\n","import { LogLevel } from './Logger';\nimport { PlayerControllerImpl } from './PlayerControllerImpl';\n\ndeclare global {\n\tinterface Window {\n\t\tonYouTubeIframeAPIReady(): void;\n\t}\n}\n\nenum PlayerState {\n\tUNSTARTED = -1,\n\tENDED = 0,\n\tPLAYING = 1,\n\tPAUSED = 2,\n\tBUFFERING = 3,\n\tCUED = 5,\n}\n\n// https://github.com/VocaDB/vocadb/blob/076dac9f0808aba5da7332209fdfd2ff4e12c235/VocaDbWeb/Scripts/ViewModels/PVs/PVPlayerYoutube.ts.\nexport class YouTubePlayerController extends PlayerControllerImpl<YT.Player> {\n\tprivate previousTime?: number;\n\n\tprivate timeUpdateIntervalId?: number;\n\n\tprivate clearTimeUpdateInterval(): void {\n\t\tthis.logger.log(\n\t\t\tLogLevel.Debug,\n\t\t\t'clearTimeUpdateInterval',\n\t\t\tthis.timeUpdateIntervalId,\n\t\t);\n\n\t\twindow.clearInterval(this.timeUpdateIntervalId);\n\n\t\tthis.timeUpdateIntervalId = undefined;\n\t}\n\n\tprivate invokeTimeUpdate(player: YT.Player): void {\n\t\tconst currentTime = player.getCurrentTime();\n\t\tif (currentTime === this.previousTime) return;\n\n\t\tconst duration = player.getDuration();\n\t\tthis.options?.onTimeUpdate?.({\n\t\t\tduration: duration,\n\t\t\tpercent: currentTime / duration,\n\t\t\tseconds: currentTime,\n\t\t});\n\n\t\tthis.previousTime = currentTime;\n\t}\n\n\tprivate setTimeUpdateInterval(): void {\n\t\tthis.logger.log(LogLevel.Debug, 'setTimeUpdateInterval');\n\n\t\tthis.clearTimeUpdateInterval();\n\n\t\tthis.timeUpdateIntervalId = window.setInterval(\n\t\t\t() => this.invokeTimeUpdate(this.player),\n\t\t\t250,\n\t\t);\n\n\t\tthis.logger.log(\n\t\t\tLogLevel.Debug,\n\t\t\t'timeUpdateIntervalId',\n\t\t\tthis.timeUpdateIntervalId,\n\t\t);\n\n\t\tthis.invokeTimeUpdate(this.player);\n\t}\n\n\tattach(id: string): Promise<void> {\n\t\treturn new Promise((resolve, reject /* TODO: reject */) => {\n\t\t\tthis.player.addEventListener('onReady', async () => {\n\t\t\t\tthis.player.addEventListener('onError', (event) =>\n\t\t\t\t\tthis.options?.onError?.(event.data),\n\t\t\t\t);\n\t\t\t\tthis.player.addEventListener(\n\t\t\t\t\t'onStateChange',\n\t\t\t\t\t(event: YT.EventArgs): void => {\n\t\t\t\t\t\tthis.logger.log(\n\t\t\t\t\t\t\tLogLevel.Debug,\n\t\t\t\t\t\t\t`state changed: ${PlayerState[event.data]}`,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tswitch (event.data) {\n\t\t\t\t\t\t\tcase YT.PlayerState.CUED:\n\t\t\t\t\t\t\t\tthis.options?.onLoaded?.({ id: id });\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tcase YT.PlayerState.PLAYING:\n\t\t\t\t\t\t\t\tthis.options?.onPlay?.();\n\t\t\t\t\t\t\t\tthis.setTimeUpdateInterval();\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tcase YT.PlayerState.PAUSED:\n\t\t\t\t\t\t\t\tthis.options?.onPause?.();\n\t\t\t\t\t\t\t\tthis.clearTimeUpdateInterval();\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tcase YT.Pl