UNPKG

@speechkit/speechkit-audio-player

Version:

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

505 lines (422 loc) 13.5 kB
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