UNPKG

@speechkit/speechkit-audio-player

Version:

A web player component that can play audio from https://speechkit.io

464 lines (392 loc) 14.1 kB
import { compact, forEach } from 'lodash' import AudioPlayer from './AudioPlayer' import timeFormatter from '../utils/TimeFormatter' import translate from '../utils/Translate' import { setHTML, addClassName, removeClassName } from '../utils/html' // Assets import playImage from './images/play.svg' import playKickerImage from './images/play-kicker.svg' import pauseImage from './images/pause.svg' import pauseKickerImage from './images/pause-kicker.svg' import backwardButton from './images/backward.svg' import forwardButton from './images/forward.svg' import podcastButton from './images/podcast.svg' import speed05Icon from './images/0-5-speed.svg' import speed1Icon from './images/1-speed.svg' import speed15Icon from './images/1-5-speed.svg' import speed2Icon from './images/2-speed.svg' import speakerIconCircle from './images/speaker-circle.svg' import infoIcon from './images/info.svg' import robotIcon from './images/robot.svg' import downloadIcon from './images/download.svg' import playerTypes from '../constants/playerTypes' const CONTAINER_WITH_LOGO_CLASS = 'speechkit__container--withLogo' const ADS_PLAYING_CLASS = 'ads-playing' const BLACK_COLOR = '#000' const DEFAULT_BAR_COLOR = '#B86BC6' const SPEEDS = [0.5, 1, 1.5, 2] const SPEEDS_ICONS = { '0.5': speed05Icon, '1': speed1Icon, '1.5': speed15Icon, '2': speed2Icon, } const aCustomFeedbackLinkIDs = [1986] const aHideSKLinkIDs = [1757,4619] const aHideFeedbackLinkIDs = [4619] const KICKER_ID = 5332 let downloadLink class UIPlayer extends AudioPlayer { get isMinimal() { return this.options.player === playerTypes.MINIMAL } get isPlayList() { return this.options.player === playerTypes.PLAYLIST } render() { // 1. Get and style container this.container = document.getElementById(this.renderNode) if (!this.container) { throw new Error(`Could not find #${rootNode} to mount player on... Please specify an html id that exists`) } addClassName(this.container, 'speechkit__render-node') // 2. Build the player view const { skBackend, feedbackUrl, podcast, podcastUrl, publisher, publisherId, publisherLogo, message, language, visibleItems, withDownloadButton, } = this.options const playerTitle = translate(language, this.isMinimal ? 'shortTitle': 'title') if (withDownloadButton === true) { const { media } = podcast || { media: [{}] } const { download_url: downloadUrl } = media.find(({ download_url }) => download_url) || {} if (downloadUrl) { downloadLink = `<a href='${downloadUrl}' target='_blank' rel='noopener' download>${downloadIcon}</a>` } } const customFeedbackURL = aCustomFeedbackLinkIDs.includes(Number.parseInt(publisherId)) const feedbackURLIsHidden = aHideFeedbackLinkIDs.includes(Number.parseInt(publisherId)) const SKLinkIsHidden = aHideSKLinkIDs.includes(Number.parseInt(publisherId)) this.isKicker = publisherId == KICKER_ID this.container.innerHTML = this.buildPlayerView({ skBackend, feedbackUrl, customFeedbackURL, feedbackURLIsHidden, SKLinkIsHidden, backwardButton, forwardButton, podcastButton, podcastUrl, publisher, publisherLogo, speakerIconCircle, infoIcon, robotIcon, speed1Icon, playerTitle, message, type: this.playerStyleType, visibleItems, downloadLink, publisherColor: this.getCurrentColor(), }) addClassName(this.container, 'is-mobile-version', this.isMobileVersion) addClassName(this.container, 'is-amp', this.options.isAmp) addClassName(this.container, this.options.player) // 3. Add in the audio tag this.destroyAudio() const bAdsDisabled = (podcast && podcast.ad_disabled) if (!bAdsDisabled && !this.adsService.isPlayed()) { return this.adsService .getAds() .then(data => { this.buildAudio(this.container, data ? this.mediaAds : this.media) }) .catch(() => { this.buildAudio(this.container, this.media) }) .then(() => { this.setupUI() }) } this.buildAudio(this.container, this.media) this.setupUI() } switchToPodcast(withBeepSound = true) { this.destroyAudio() this.buildAudio(this.container, this.media) if (withBeepSound) { this.playBeep().then(() => { this.play() }) } else { this.play() } if (this.shouldPlayInTime) { this.setCurrentTime(this.shouldPlayInTime) this.setPodcastCurrentTime(0) } } setupUI() { this.setupUIBindings() this.setupUIEvents() this.setupCustomizations() } setupUIBindings() { this.playPauseButton = this.container.getElementsByClassName('speechkit__shared__controls__playpause')[0] } setupUIEvents() { // Setup common control listeners const { playlistPlayPauseButtons, playPauseButton, backwardButton, forwardButton, speedButton, progressbarWrapper, skipForward, skipBackward, advertiserLogo, advertiserLink, } = this.getCommonUIControls() playPauseButton && playPauseButton.addEventListener('click', this.clickPlayPause.bind(this)) backwardButton && backwardButton.addEventListener('click', this.skipCurrentTime.bind(this, -15)) forwardButton && forwardButton.addEventListener('click', this.skipCurrentTime.bind(this, 15)) speedButton && speedButton.addEventListener('click', this.onSpeedButtonClick.bind(this)) progressbarWrapper && progressbarWrapper.addEventListener('click', this.onProgressBarClick.bind(this)) skipForward && skipForward.addEventListener('click', this.playNext.bind(this)) skipBackward && skipBackward.addEventListener('click', this.playPrevious.bind(this)) advertiserLogo && advertiserLogo.addEventListener('click', this.handleClickAdLogo.bind(this)) this.clickPlaylistItem = this.clickPlaylistItem.bind(this) if (advertiserLink) { Array.from(advertiserLink).forEach(link => { link.addEventListener('click', () => { this.handleClickAdLink() }) }) } if (playlistPlayPauseButtons && playlistPlayPauseButtons.length) { Array.from(playlistPlayPauseButtons).forEach(button => { if (button.parentNode && button.parentNode.tagName === 'LI') { button.parentNode.addEventListener('click', () => { this.clickPlaylistItem({ currentTarget: button }) }) } else { button.addEventListener('click', this.clickPlaylistItem) } }) } this.updateUIState() this.updateProgress() } getCurrentColor() { const defaultColor = this.isMinimal ? BLACK_COLOR : DEFAULT_BAR_COLOR return this.options.publisherColor || defaultColor } setupCustomizations() { const currentColor = this.getCurrentColor() const commonControls = this.getCommonUIControls() const itemsToColor = compact([commonControls.progressBar, commonControls.authorContainer]) const { renderNode, } = this document.getElementById(renderNode).style.color = currentColor forEach(itemsToColor, (item) => { (item.innerText || item.textContent).trim().length > 0 ? item.style.color = currentColor : item.style.backgroundColor = currentColor }) if (this.playlistItems && this.playlistItems.length && this.options.player === playerTypes.PLAYLIST) { commonControls.playPauseButton.style.color = currentColor if (this.isKicker) { const { playListBox } = this.getCommonUIControls() playListBox.classList.add('list-customizations') } const currentItem = this.playlistItems[this.currentPlaylistPodcastIndex] if (currentItem && currentItem.parentNode) { const parent = currentItem.parentNode const position = currentItem.offsetTop - parent.offsetTop - currentItem.offsetHeight if (parent.scrollTo) { parent.scrollTo(0, position) } else { parent.scrollTop = position } } } } /** * UI Actions */ clickPlayPause() { this.isPlaying() ? this.pause() : this.play() } clickPlaylistItem({ currentTarget }) { const index = Number(currentTarget.dataset.index) if (this.currentPlaylistPodcastIndex === index) { return this.clickPlayPause() } this.currentPlaylistPodcastIndex = index this.pause() this.loadPodcast(this.podcasts[index]).then(() => { this.play() }) } speedUpPlayer() { const speeds = SPEEDS const currentSpeed = this.getSpeed() this.speed = speeds[(speeds.indexOf(currentSpeed) + 1) % speeds.length] this.player.playbackRate = this.speed this.analytics.trackDidChangeSpeed(this.getSpeed()) } updateProgress() { this.progressDidUpdate(this.getCurrentTime(), this.getDuration()) } updateAdsUI() { const renderNode = document.getElementById(this.renderNode) if (this.adsService.ads && this.mediaAds) { const { advertiserLogo, advertiserName, advertiserLink, playerContainer, adsTitle } = this.getCommonUIControls() const { logo, promo_link: promoLink, title } = this.mediaAds const img = advertiserLogo ? advertiserLogo.getElementsByTagName('img')[0] : {} setHTML(advertiserName, this.adsService.ads.campaign_name) setHTML(adsTitle, title) if (logo) { addClassName(playerContainer, CONTAINER_WITH_LOGO_CLASS) img.src = logo } else { advertiserLogo && advertiserLogo.remove() } if (advertiserLogo && promoLink && logo) { advertiserLogo.setAttribute('href', promoLink) } if (advertiserLink && advertiserLink.length && promoLink) { Array().forEach.call(advertiserLink, link => { link.setAttribute('href', promoLink) }) } return addClassName(renderNode, ADS_PLAYING_CLASS) } removeClassName(renderNode, ADS_PLAYING_CLASS) if (this.adsService.adsHistory) { const { advertiserTitle } = this.getCommonUIControls() return addClassName(advertiserTitle, 'visible') } } get playPauseButtonImage() { const { isKicker, state } = this const { loading, playing } = state if (loading || playing) { return isKicker ? pauseKickerImage : pauseImage } return isKicker ? playKickerImage : playImage } updateUIState() { const { loading, playing } = this.state const { language } = this.options const { title, playPauseButton, adsNote, advertiserNote } = this.getCommonUIControls() let message = translate(language, this.isMinimal ? 'shortTitle' : 'title') let buttonLabel = 'Play' if(this.isPlaying()) { buttonLabel = 'Pause' } if (loading) message = translate(language, 'loading') if (playing) message = translate(language, 'playing') if (adsNote) setHTML(adsNote, translate(language, 'adsNote')) if (advertiserNote) setHTML(advertiserNote, translate(language, 'advertiserNote')) if (title) setHTML(title, message) playPauseButton.setAttribute('aria-label', buttonLabel) playPauseButton.innerHTML = this.playPauseButtonImage } /** * Shared UI event handlers * Override for custom implementation */ onProgressBarClick(event) { if (this.adsService.isPlaying()) return const rect = event.currentTarget.getBoundingClientRect() const x = rect.x || rect.left const clickPos = event.pageX - x const width = event.currentTarget.offsetWidth this.setCurrentTimePercent(clickPos / width) this.updateProgress() } onSpeedButtonClick(event) { if (!this.adsService.isPlaying()) { this.speedUpPlayer() const speed = this.getSpeed() if (this.isMinimal) { return setHTML(event.currentTarget, SPEEDS_ICONS[speed]) } setHTML(event.currentTarget, `${speed}<small>X</small>`) } } progressDidUpdate(currentTime, duration) { if (isNaN(currentTime)) { currentTime = 0 } const commonControls = this.getCommonUIControls() const progressBar = commonControls.progressBar const loadingProgressBar = commonControls.loadingProgressBar const timeCurrent = commonControls.timeCurrent const timeCurrentCountDown = commonControls.timeCurrentCountDown const timeTotal = commonControls.timeTotal const percent = currentTime * 100 / duration let percentLoading = null if (this.loadedFragment) { percentLoading = this.loadedFragment.endDTS * 100 / duration } loadingProgressBar && percentLoading && (loadingProgressBar.style.width = `${percentLoading}%`) progressBar && (progressBar.style.width = `${percent}%`) setHTML(timeCurrent, timeFormatter(currentTime)) setHTML(timeCurrentCountDown, timeFormatter(duration - currentTime)) setHTML(timeTotal, duration ? timeFormatter(duration) : '--:--') } playNext() { if (!this.hasNext()) { return } this.loadNextPodcast().then(() => { this.play() }) } playPrevious() { if (!this.hasPrevious()) { return } this.loadPreviousPodcast().play() } handleClickAdLogo() { this.analytics.trackAdLogoClick() } handleClickAdLink() { this.analytics.trackAdLinkClick() } /** * Override in subclasses */ getCommonUIControls() { return { playerContainer: null, playPauseButton: null, backwardButton: null, forwardButton: null, skipForward: null, skipBackward: null, speedButton: null, progressbarWrapper: null, progressBar: null, loadingProgressBar: null, timeCurrent: null, timeCurrentCountDown: null, timeTotal: null, authorContainer: null, playerTitle: null, advertiserTitle: null, advertiserLogo: null, advertiserName: null, advertiserLink: null, adsTitle: null, adsNote: null, advertiserNote: null, playlistPlayPauseButtons: [], } } } export default UIPlayer