@speechkit/speechkit-audio-player
Version:
A web player component that can play audio from https://speechkit.io
464 lines (392 loc) • 14.1 kB
JavaScript
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