Skip to content
This repository has been archived by the owner on Jun 28, 2021. It is now read-only.

Simplify audioplayer #360

Merged
merged 15 commits into from
Jul 3, 2016
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
NODE_ENV=development
PORT=8000
API_URL=http://quran.com:3000
API_URL=http://localhost:3000
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill change it back!

SEGMENTS_KEY=
SENTRY_KEY_CLIENT=
SENTRY_KEY_SERVER=
13 changes: 13 additions & 0 deletions src/components/Audioplayer/Loader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

export default () => (
<div className="sequence">
<div className="seq-preloader">
<svg height="16" width="42" className="seq-preload-indicator" xmlns="http://www.w3.org/2000/svg">
<circle className="seq-preload-circle seq-preload-circle-1" cx="6" cy="8" r="5" />
<circle className="seq-preload-circle seq-preload-circle-2" cx="20" cy="8" r="5" />
<circle className="seq-preload-circle seq-preload-circle-3" cx="34" cy="8" r="5" />
</svg>
</div>
</div>
);
39 changes: 39 additions & 0 deletions src/components/Audioplayer/RepeatButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { PropTypes } from 'react';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';

const style = require('../style.scss');

const RepeatButton = ({ shouldRepeat, onRepeatToggle }) => {
const tooltip = (
<Tooltip id="repeat-button-tooltip">
Repeats the current ayah on end...
</Tooltip>
);

return (
<div className="text-center">
<input type="checkbox" id="repeat" className={style.checkbox} />
<OverlayTrigger
overlay={tooltip}
placement="right"
trigger={['hover', 'focus']}
>
<label
htmlFor="repeat"
className={`pointer ${style.buttons} ${shouldRepeat ? style.repeat : ''}`}
onClick={onRepeatToggle}
>
<i className="ss-icon ss-repeat" />
</label>
</OverlayTrigger>
</div>
);
};

RepeatButton.propTypes = {
shouldRepeat: PropTypes.bool.isRequired,
onRepeatToggle: PropTypes.func.isRequired
};

export default RepeatButton;
39 changes: 39 additions & 0 deletions src/components/Audioplayer/ScrollButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { PropTypes } from 'react';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';

const style = require('../style.scss');

const ScrollButton = ({ shouldScroll, onScrollToggle }) => {
const tooltip = (
<Tooltip id="scroll-button-tooltip">
Automatically scrolls to the currently playing ayah on transitions...
</Tooltip>
);

return (
<div className="text-center">
<input type="checkbox" id="scroll" className={style.checkbox} />
<OverlayTrigger
overlay={tooltip}
placement="right"
trigger={['hover', 'focus']}
>
<label
htmlFor="scroll"
className={`pointer ${style.buttons} ${shouldScroll ? style.scroll : ''}`}
onClick={onScrollToggle}
>
<i className="ss-icon ss-link" />
</label>
</OverlayTrigger>
</div>
);
};

ScrollButton.propTypes = {
shouldScroll: PropTypes.bool.isRequired,
onScrollToggle: PropTypes.func.isRequired
};

export default ScrollButton;
193 changes: 46 additions & 147 deletions src/components/Audioplayer/Segments/index.js
Original file line number Diff line number Diff line change
@@ -1,113 +1,79 @@
import React, { Component, PropTypes } from 'react';
import { decrypt } from 'sjcl';
import Helmet from 'react-helmet';

export default class Segments extends Component {
static propTypes = {
audio: PropTypes.object.isRequired,
segments: PropTypes.string.isRequired,
currentAyah: PropTypes.string,
currentWord: PropTypes.string,
currentTime: PropTypes.number,
onSetCurrentWord: PropTypes.func.isRequired
};

static defaultProps = {
currentWord: null
};

constructor() {
super(...arguments); // eslint-disable-line prefer-rest-params
this.secret = process.env.SEGMENTS_KEY;
this.currentWord = null;
}

state = {
intervals: [],
words: {},
};

// LIFECYCLE METHODS

componentDidMount() {
this.bindListeners();
this.buildIntervals(this.props);
}

componentWillReceiveProps(nextProps) {
const prevProps = this.props;

if (prevProps.audio !== nextProps.audio) {
this.unbindListeners(prevProps);

if (this.props.audio !== nextProps.audio) {
this.buildIntervals(nextProps);
this.bindListeners(nextProps);
}
}

shouldComponentUpdate(nextProps, nextState) {
const prevProps = this.props;
const prevState = this.state;
if (this.props.currentTime !== nextProps.currentTime) {
if (this.state.words) {
let currentWord = null;

return [
prevProps.audio !== nextProps.audio,
prevProps.segments !== nextProps.segments,
prevState.intervals !== nextState.intervals,
prevProps.currentWord !== nextProps.currentWord,
nextProps.currentWord !== this.currentWord
].some(b => b);
// TODO: I think we can just 'return false' here since there is nothing to actually render...
// oh wait, maybe i need it so that componentDidUpdate will run..., despite render() not
// actually being needed... dunno right now
}
Object.keys(this.state.words).forEach(wordIndex => {
const word = this.state.words[wordIndex];

componentDidUpdate() {
if (this.currentWord !== this.props.currentWord) { // currentWord was changed by the user
if (this.props.currentWord !== null) {
const wordInterval = this.state.words[this.props.currentWord.split(/:/).pop()];
// seek to the currentWord starting time and return
const timeToSeek = wordInterval.startTime + 0.001;
const isSeekable = this.props.audio.seekable && this.props.audio.seekable.length > 0;
const withinRange = !isSeekable ?
null :
(timeToSeek >= this.props.audio.seekable.start(0) &&
timeToSeek <= this.props.audio.seekable.end(0));

if (isSeekable && withinRange) { // seek to it
this.props.audio.currentTime = timeToSeek;
} else { // seek to it after its ready
const seekToTime = () => {
this.props.audio.currentTime = timeToSeek;
this.props.audio.removeEventListener('canplay', seekToTime, false);
};
this.props.audio.addEventListener('canplay', seekToTime);
}
}
if (nextProps.currentTime > word.startTime && nextProps.currentTime < word.endTime) {
currentWord = `${nextProps.currentAyah}:${wordIndex}`;
}
});

// but don't forget to set the change internally for next time
return this.setCurrentWord(this.props.currentWord, 'componentDidUpdate');
this.props.onSetCurrentWord(currentWord);
}
}

return false;
}

componentWillUnmount() {
this.unbindListeners();
shouldComponentUpdate(nextProps) {
return [
this.props.audio !== nextProps.audio,
this.props.currentAyah !== nextProps.currentAyah,
this.props.currentWord !== nextProps.currentWord,
].some(test => test);
}

setCurrentWord(currentWord = null) {
// this is more immediately available but should eventually agree with props
this.currentWord = currentWord;
// calls the redux dispatch function passed down from the Audioplayer
this.props.onSetCurrentWord(currentWord);
}
buildIntervals(props) {
const { segments } = props;

buildIntervals(props = this.props) {
let segments = null;
let parsedSegments = null;
try {
segments = JSON.parse(decrypt(this.secret, new Buffer(props.segments, 'base64').toString()));
parsedSegments = JSON.parse(
decrypt(
process.env.SEGMENTS_KEY,
new Buffer(segments, 'base64').toString()
)
);
} catch (e) {
segments = [];
parsedSegments = [];
}

const words = {};
const intervals = segments.map(segment => {
const intervals = parsedSegments.map(segment => {
const startTime = segment[0];
const endTime = segment[0] + segment[1];
const duration = segment[1];
Expand All @@ -130,90 +96,23 @@ export default class Segments extends Component {
return { intervals, words }; // for console debugging
}

bindListeners() {
const { audio, currentAyah, currentWord } = this.props;
const { intervals } = this.state;

// Play listener
const play = () => {
const listeners = {};
let repeaterId = null;

new Promise((done, fail) => {
const intervalFn = () => {
if (audio.seeking) return console.warn('we are seeking right now?');
if (audio.paused || audio.ended) return console.warn('stopped by running?');

// Was thinking about adding some initial checks before this that could reduce
// the number of times we need to resort to a search, just in case logarithmic
// time isn't good enough
const index = this.binarySearch(intervals, audio.currentTime, this.compareFn);
const word = index >= 0 && intervals[index][2] >= 0 ?
`${currentAyah}:${intervals[index][2]}` :
null;

if (word === currentWord) return false; // no work to be done
else if (word === this.currentWord) return false; // still no work to be done
// something changed, so we deal with it
return this.setCurrentWord(word, 'Play listener Do Stuff block');
};

intervalFn();
repeaterId = setInterval(intervalFn, 30);

['pause', 'ended'].forEach((evName) => {
listeners[evName] = done;
audio.addEventListener(evName, listeners[evName], false);
});

['error', 'emptied', 'abort'].forEach((evName) => {
listeners[evName] = fail;
audio.addEventListener(evName, listeners[evName], false);
});
}).then(() => {
clearInterval(repeaterId);

['pause', 'ended', 'error', 'emptied', 'abort'].forEach((evName) => {
audio.removeEventListener(evName, listeners[evName]);
});
render() {
const { currentWord } = this.props;
const style = [];

if (currentWord) {
style.push({
cssText: `#word-${currentWord.replace(/:/g, '-')}{
color: #279197;
border-color: #279197;
}`
});
};
audio.addEventListener('play', play, false);

this.setState({ listeners: { play }});
}

unbindListeners() {
const { audio } = this.props;

audio.removeEventListener('play', this.state.listeners.play);
}

compareFn(time, interval) {
if (time < interval[0]) return -1;
else if (time > interval[1]) return 1;
else if (time === interval[0]) return 0; // floor inclusive
else if (time === interval[1]) return 1;

return 0;
}

binarySearch(ar, el, compareFn = (a, b) => (a - b)) {
let m = 0;
let n = ar.length - 1;
while (m <= n) {
const k = (n + m) >> 1;
const cmp = compareFn(el, ar[k]);
if (cmp > 0) m = k + 1;
else if (cmp < 0) n = k - 1;
else return k;
}
return -m - 1;
}

render() {
return (
<input type="hidden" />
<Helmet
style={style}
/>
);
}
}
26 changes: 0 additions & 26 deletions src/components/Audioplayer/Track/Tracker/index.js

This file was deleted.

Loading