@speechkit/speechkit-audio-player
Version:
A web player component that can play audio from https://speechkit.io
505 lines (422 loc) • 13.5 kB
JavaScript
import { defaults, indexOf, find } from 'lodash'
import {
IFRAME_ATTRS, MINIMAL_STYLE_IFRAME_ATTRS, SPECIAL_ATTRS, FORBES_ATTRS, PLAYLIST_STYLE_IFRAME_ATTRS,
} from '../constants/iframeAttributes'
import MEDIA_TYPES from '../constants/mediaTypes'
import Analytics from '../Analytics'
import PodcastResolver from '../SKSDK/PodcastResolver'
import AdsService from '../SKSDK/AdsService'
import UIPlayer from './UIPlayer'
import playerJs from 'player.js'
import playerTypes from '../constants/playerTypes'
import { optimizedResize, filterPodcasts } from '../utils'
const mq = window.matchMedia('screen and (min-width: 320px) and (max-width: 499px)')
class PlayerBase extends UIPlayer {
/**
* @param {Object} options An object with options for the player
* options.renderNode: The id to the node where to render the player (use this only to render the default player)
* options.podcast: A speechkit podcast response
* options.podcasts: An array of speechkit podcasts
* options.analyticsId: The analyticsId for SpeechKit
* options.analyticsKey: THe analytics key provided by SpeechKit
*/
constructor(options) {
super()
this.ads = null
this.options = defaults({}, options)
this.currentPodcastIndex = 0
this.podcastResolver = new PodcastResolver(options)
this.adsService = new AdsService(options)
this.mediaType = MEDIA_TYPES.PODCAST
this.playerStyleType = options.type
this.isMobileVersion = false
this.podcasts = options.podcasts ? filterPodcasts(options.podcasts) : [options.podcast]
this.renderNode = options.renderNode
// Set us Sentry Scope
if (window.Sentry) {
Sentry.configureScope((scope) => {
scope.setTag("version", speechkit.player.default.version);
scope.setTag("type", this.options.player); //eg.MinimalPlayer
scope.setTag("publisherId", this.options.publisherId); //eg.MinimalPlayer
scope.setTag("projectId", this.options.projectId); //eg.MinimalPlayer
});
}
// Set up player.js
this.playerJsReceiver = new playerJs.Receiver()
this.setupPlayerJsMethods()
// Instance variables that will be set
this.analytics = null
this.container = null
this.playPauseButton = null
const publisher = find(Object.keys(SPECIAL_ATTRS), publisherDomain => document.referrer.indexOf(publisherDomain) !== -1)
if (publisher) {
const attrs = SPECIAL_ATTRS[publisher]
this.isMobileVersion = attrs.isMobileVersion
if (this.isMinimal) {
return this.postMessage({
attrs: {
style: attrs.minimalStyle
}
})
}
return this.postMessage({
attrs: {
style: mq.matches ? attrs.mobileStyle : attrs.style,
onload: attrs.onload
}
})
}
// Forbes only: click listener to change bg colour
const iframeClick = () => {
document.body.style.background = 'rgba(255, 255, 255, 1)';
window.removeEventListener('click', iframeClick);
}
// custom code to apply forbes formatting to
const publisherIsForbes = this.options && this.options.publisher && this.options.publisherId ==='3265';
const attrs = publisherIsForbes ? FORBES_ATTRS :
this.isMinimal ? MINIMAL_STYLE_IFRAME_ATTRS :
this.isPlayList ? PLAYLIST_STYLE_IFRAME_ATTRS :
IFRAME_ATTRS
this.postMessage({
attrs: {
style: mq.matches ? attrs.mobileStyle : attrs.style,
onload: attrs.onload
}
})
if(publisherIsForbes) {
window.addEventListener('click', iframeClick, { passive: false });
}
this.loadAdditionalMedia()
optimizedResize.add(() => {
this.postCurrentHeight()
})
const publirBox = document.getElementById('publir-box')
if (publirBox && window.MutationObserver) {
this.observer_ = new MutationObserver(() => {
this.postCurrentHeight()
})
this.observer_.observe(publirBox, { attributes: true, childList: true, characterData: true, subtree: true })
}
}
postCurrentHeight = () => {
const $body = document.querySelector('body')
const height = $body.getBoundingClientRect().height
if (height) {
try {
this.postMessage({
msg: 'iframe-resize',
attrs: { height: `${height}px` },
})
this.postMessage(JSON.stringify({
src: window.location.toString(),
context: 'iframe.resize',
height,
}))
if (this.options.isAmp) {
this.postMessage(JSON.stringify({
sentinel: 'amp',
type: 'embed-size',
height,
}))
}
} catch (e) {
console.log(e)
}
}
}
checkNextPodcast() {
const { podcast } = this
if (podcast && podcast.next_podcast_external_id) {
return this.podcastResolver.loadNext(podcast.next_podcast_external_id)
.then(response => {
this.nextPodcast = response
})
.catch(() => {
this.nextPodcast = null
})
}
this.nextPodcast = null
}
/**
* Media related
*/
load() {
if (this.podcasts[0] || this.options.isDemo) {
this.loadPodcast(this.podcasts[0])
} else {
this.loadPodcastForUrl(this.options.articleUrl)
}
return this
}
loadNextPodcast() {
return this.loadPodcastWithIndex(Math.min(this.getPlaylistSize() - 1, this.currentPodcastIndex + 1))
}
loadPreviousPodcast() {
return this.loadPodcastWithIndex(Math.max(0, this.currentPodcastIndex - 1))
}
loadPodcastWithIndex(index) {
this.currentPlaylistPodcastIndex = index
return this.loadPodcast(this.podcasts[index])
}
loadPodcast(podcast) {
return new Promise(resolve => {
if (this.options.isDemo) {
const audio = new Audio(this.options.preview)
audio.volume = 1
audio.addEventListener('loadedmetadata', () => {
this.demoAudio = audio
this.updatePlayer(podcast)
resolve()
})
return
}
const bAdsDisabled = (podcast && podcast.ad_disabled)
if (!bAdsDisabled && !this.adsService.isPlayed()) {
return this.adsService.getAds()
.then(data => {
if (data) {
this.mediaAds = this.getMediaForPodcast(data)
this.mediaType = MEDIA_TYPES.PREROLL
this.preloadLogo()
} else {
this.mediaType = MEDIA_TYPES.PODCAST
}
}).catch(() => {
this.mediaType = MEDIA_TYPES.PODCAST
}).then(() => {
this.updatePlayer(podcast)
resolve()
})
} else {
this.mediaType = MEDIA_TYPES.PODCAST
this.updatePlayer(podcast)
}
resolve()
})
}
loadPodcastForUrl(url) {
this.podcastResolver.resolve(url)
.then((podcast) => {
if (podcast) {
this.podcasts = [podcast]
this.loadPodcast(podcast)
} else {
this.postMessage('sk-fail')
}
}).catch((e) => {
this.postMessage('sk-fail')
if(!e) {
console.log('Please verify corresponding audio exists for this article.');
return;
}
throw new Error('Error Resolving Speechkit Audio: ' + e);
})
return this
}
updatePlayer(podcast) {
this.currentPodcastIndex = indexOf(this.podcasts, podcast)
this.podcast = podcast
this.checkNextPodcast()
this.media = this.getMediaForPodcast(podcast)
if (!this.media) {
return this.postMessage('sk-fail')
}
this.analytics = new Analytics({
options: this.options,
podcast: this.podcast,
media: this.media,
mediaAds: this.mediaAds,
ads: this.adsService.ads,
mediaType: this.mediaType,
player: this,
})
this.render()
this.postMessage('sk-success')
this.playerJsReceiver.ready()
this.emit('didLoad', this.podcast)
this.postCurrentHeight()
if (!this.nextPodcastIsPlaying) {
this.analytics.trackDidLoad({
media_id: this.media.id,
media_type: MEDIA_TYPES.PODCAST
})
}
}
preloadLogo() {
if (this.isMinimal) return
try {
const image = new Image()
image.src = this.mediaAds.logo
} catch(e) {}
}
getDuration() {
let duration = super.getDuration()
if (duration) {
return duration
}
if (this.media && this.media.duration) {
return this.media.duration
}
}
getPlaylistSize() {
return this.podcasts.length
}
hasNext() {
return this.currentPodcastIndex < this.getPlaylistSize() - 1
}
hasPrevious() {
return this.currentPodcastIndex > 0
}
playBeep() {
return this.playSoundSnippet(this.beepSound)
}
/**
* Handle events
*/
didPlay = () => {
clearTimeout(this.loadingTimeout)
if (this.currentTime) {
this.setCurrentTime(this.currentTime)
}
this.setState({
loading: false,
playing: true
})
this.updateUIState()
this.updateAdsUI()
// TODO: Handle resume events here. Checking the time doesn't reliably show
// play events. Sending a "play" event here means every time, including resumes.
if (this.mediaType === MEDIA_TYPES.PREROLL && !this.adsService.adsIsPlaying) {
this.analytics.trackDidLoad()
}
if (this.adsService.ads) {
this.adsService.setPlaying()
}
this.analytics.trackDidPlay()
this.playerJsReceiver.emit('play')
this.emit('didPlay', this.podcast)
}
didPause = () => {
this.setState({ playing: false })
this.updateUIState()
// Make sure it isn't at the end...
if (this.getCurrentTime() < this.getDuration()) {
this.analytics.trackDidPause(this.getCurrentTime(), this.getCurrentPercentage())
this.emit('didPause', this.podcast)
}
this.playerJsReceiver.emit('pause')
}
didEnd = () => {
this.pause()
this.updateUIState()
this.analytics.trackDidListenToEnd()
this.playerJsReceiver.emit('ended')
if (this.adsService.ads) {
this.setState({ loaded: false })
this.resetAdsAndPlayPodcast()
}
if (this.nextPodcast && this.options.player !== playerTypes.PLAYLIST) {
this.playBeep().then(() => {
this.loadPodcast(this.nextPodcast).then(() => {
this.play()
})
})
this.nextPodcastIsPlaying = true
} else if (this.options.player === playerTypes.PLAYLIST && this.hasNext()) {
this.playBeep().then(() => {
this.loadNextPodcast().then(() => {
this.play()
})
})
}
this.emit('didEnd', this.podcast)
}
didLoadMetadata = () => {
this.setState({
metadataloaded: true
})
this.updateProgress()
}
didTimeUpdate = () => {
const currentTime = this.getCurrentTime()
const duration = this.getDuration()
this.progressDidUpdate(currentTime, duration)
this.playerJsReceiver.emit('timeupdate', { seconds: currentTime, duration })
this.emit('didProgress', { currentTime, duration, podcast: this.podcast })
}
handleWaiting = () => {
clearTimeout(this.loadingTimeout)
this.loadingTimeout = setTimeout(() => {
if (!this.isEnded()) {
this.setState({ loading: true, playing: false })
this.updateUIState()
}
}, 1000)
}
postMessage(data) {
if (parent) {
parent.postMessage(data, "*")
}
}
loadAdditionalMedia() {
// NOTE: IE don't support this mime type
// https://stackoverflow.com/questions/39354085/how-to-play-wav-files-on-ie
import('./sounds/beep.wav').then(beepSound => {
this.beepSound = beepSound
})
}
resetAdsAndPlayPodcast(withBeepSound) {
this.setState({ loading: true })
this.adsService.setPlayed()
this.adsService.removeAds()
this.mediaAds = null
this.updateAdsUI()
this.switchToPodcast(withBeepSound)
this.updateUIState()
this.mediaType = MEDIA_TYPES.PODCAST
this.analytics.setMediaType(this.mediaType)
}
// Playerjs methods
setupPlayerJsMethods() {
this.playerJsReceiver.on('play', function () {
this.play()
}.bind(this))
this.playerJsReceiver.on('pause', function () {
this.pause()
}.bind(this))
this.playerJsReceiver.on('getPaused', function (callback) {
callback(!this.isPlaying())
}.bind(this))
this.playerJsReceiver.on('getCurrentTime', function (callback) {
callback(this.getCurrentTime())
}.bind(this))
this.playerJsReceiver.on('setCurrentTime', function (value) {
this.setCurrentTime(value)
}.bind(this))
this.playerJsReceiver.on('getDuration', function (callback) {
callback(this.getDuration())
}.bind(this))
this.playerJsReceiver.on('getVolume', function (callback) {
callback(this.getVolume() * 100)
}.bind(this))
this.playerJsReceiver.on('setVolume', function (value) {
this.setVolume(value/100)
}.bind(this))
this.playerJsReceiver.on('mute', function () {
this.mute()
}.bind(this))
this.playerJsReceiver.on('unmute', function () {
this.unmute()
}.bind(this))
this.playerJsReceiver.on('getMuted', function (callback) {
callback(this.isMuted())
}.bind(this))
this.playerJsReceiver.on('getLoop', function (callback) {
callback(this.isLoop())
}.bind(this))
this.playerJsReceiver.on('setLoop', function (value) {
this.setLoop(value)
}.bind(this))
}
}
export default PlayerBase