|
|
@ -5,131 +5,12 @@ import { formatTime } from 'flavours/glitch/features/video'; |
|
|
|
import Icon from 'flavours/glitch/components/icon'; |
|
|
|
import classNames from 'classnames'; |
|
|
|
import { throttle } from 'lodash'; |
|
|
|
import { encode, decode } from 'blurhash'; |
|
|
|
import { getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; |
|
|
|
import { debounce } from 'lodash'; |
|
|
|
|
|
|
|
const digitCharacters = [ |
|
|
|
'0', |
|
|
|
'1', |
|
|
|
'2', |
|
|
|
'3', |
|
|
|
'4', |
|
|
|
'5', |
|
|
|
'6', |
|
|
|
'7', |
|
|
|
'8', |
|
|
|
'9', |
|
|
|
'A', |
|
|
|
'B', |
|
|
|
'C', |
|
|
|
'D', |
|
|
|
'E', |
|
|
|
'F', |
|
|
|
'G', |
|
|
|
'H', |
|
|
|
'I', |
|
|
|
'J', |
|
|
|
'K', |
|
|
|
'L', |
|
|
|
'M', |
|
|
|
'N', |
|
|
|
'O', |
|
|
|
'P', |
|
|
|
'Q', |
|
|
|
'R', |
|
|
|
'S', |
|
|
|
'T', |
|
|
|
'U', |
|
|
|
'V', |
|
|
|
'W', |
|
|
|
'X', |
|
|
|
'Y', |
|
|
|
'Z', |
|
|
|
'a', |
|
|
|
'b', |
|
|
|
'c', |
|
|
|
'd', |
|
|
|
'e', |
|
|
|
'f', |
|
|
|
'g', |
|
|
|
'h', |
|
|
|
'i', |
|
|
|
'j', |
|
|
|
'k', |
|
|
|
'l', |
|
|
|
'm', |
|
|
|
'n', |
|
|
|
'o', |
|
|
|
'p', |
|
|
|
'q', |
|
|
|
'r', |
|
|
|
's', |
|
|
|
't', |
|
|
|
'u', |
|
|
|
'v', |
|
|
|
'w', |
|
|
|
'x', |
|
|
|
'y', |
|
|
|
'z', |
|
|
|
'#', |
|
|
|
'$', |
|
|
|
'%', |
|
|
|
'*', |
|
|
|
'+', |
|
|
|
',', |
|
|
|
'-', |
|
|
|
'.', |
|
|
|
':', |
|
|
|
';', |
|
|
|
'=', |
|
|
|
'?', |
|
|
|
'@', |
|
|
|
'[', |
|
|
|
']', |
|
|
|
'^', |
|
|
|
'_', |
|
|
|
'{', |
|
|
|
'|', |
|
|
|
'}', |
|
|
|
'~', |
|
|
|
]; |
|
|
|
|
|
|
|
const decode83 = (str) => { |
|
|
|
let value = 0; |
|
|
|
let c, digit; |
|
|
|
|
|
|
|
for (let i = 0; i < str.length; i++) { |
|
|
|
c = str[i]; |
|
|
|
digit = digitCharacters.indexOf(c); |
|
|
|
value = value * 83 + digit; |
|
|
|
} |
|
|
|
|
|
|
|
return value; |
|
|
|
}; |
|
|
|
|
|
|
|
const decodeRGB = int => ({ |
|
|
|
r: Math.max(0, (int >> 16)), |
|
|
|
g: Math.max(0, (int >> 8) & 255), |
|
|
|
b: Math.max(0, (int & 255)), |
|
|
|
}); |
|
|
|
|
|
|
|
const luma = ({ r, g, b }) => 0.2126 * r + 0.7152 * g + 0.0722 * b; |
|
|
|
|
|
|
|
const adjustColor = ({ r, g, b }, lumaThreshold = 100) => { |
|
|
|
let delta; |
|
|
|
|
|
|
|
if (luma({ r, g, b }) >= lumaThreshold) { |
|
|
|
delta = -80; |
|
|
|
} else { |
|
|
|
delta = 80; |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
r: r + delta, |
|
|
|
g: g + delta, |
|
|
|
b: b + delta, |
|
|
|
}; |
|
|
|
const hex2rgba = (hex, alpha = 1) => { |
|
|
|
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)); |
|
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`; |
|
|
|
}; |
|
|
|
|
|
|
|
const messages = defineMessages({ |
|
|
@ -157,7 +38,9 @@ class Audio extends React.PureComponent { |
|
|
|
fullscreen: PropTypes.bool, |
|
|
|
intl: PropTypes.object.isRequired, |
|
|
|
cacheWidth: PropTypes.func, |
|
|
|
blurhash: PropTypes.string, |
|
|
|
backgroundColor: PropTypes.string, |
|
|
|
foregroundColor: PropTypes.string, |
|
|
|
accentColor: PropTypes.string, |
|
|
|
}; |
|
|
|
|
|
|
|
state = { |
|
|
@ -169,7 +52,6 @@ class Audio extends React.PureComponent { |
|
|
|
muted: false, |
|
|
|
volume: 0.5, |
|
|
|
dragging: false, |
|
|
|
color: { r: 255, g: 255, b: 255 }, |
|
|
|
}; |
|
|
|
|
|
|
|
setPlayerRef = c => { |
|
|
@ -207,10 +89,6 @@ class Audio extends React.PureComponent { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
setBlurhashCanvasRef = c => { |
|
|
|
this.blurhashCanvas = c; |
|
|
|
} |
|
|
|
|
|
|
|
setCanvasRef = c => { |
|
|
|
this.canvas = c; |
|
|
|
|
|
|
@ -221,41 +99,13 @@ class Audio extends React.PureComponent { |
|
|
|
|
|
|
|
componentDidMount () { |
|
|
|
window.addEventListener('resize', this.handleResize, { passive: true }); |
|
|
|
|
|
|
|
if (!this.props.blurhash) { |
|
|
|
const img = new Image(); |
|
|
|
img.crossOrigin = 'anonymous'; |
|
|
|
img.onload = () => this.handlePosterLoad(img); |
|
|
|
img.src = this.props.poster; |
|
|
|
} else { |
|
|
|
this._setColorScheme(); |
|
|
|
this._decodeBlurhash(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
componentDidUpdate (prevProps, prevState) { |
|
|
|
if (prevProps.poster !== this.props.poster && !this.props.blurhash) { |
|
|
|
const img = new Image(); |
|
|
|
img.crossOrigin = 'anonymous'; |
|
|
|
img.onload = () => this.handlePosterLoad(img); |
|
|
|
img.src = this.props.poster; |
|
|
|
} |
|
|
|
|
|
|
|
if (prevState.blurhash !== this.state.blurhash || prevProps.blurhash !== this.props.blurhash) { |
|
|
|
this._setColorScheme(); |
|
|
|
this._decodeBlurhash(); |
|
|
|
if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height) { |
|
|
|
this._clear(); |
|
|
|
this._draw(); |
|
|
|
} |
|
|
|
|
|
|
|
this._clear(); |
|
|
|
this._draw(); |
|
|
|
} |
|
|
|
|
|
|
|
_decodeBlurhash () { |
|
|
|
const context = this.blurhashCanvas.getContext('2d'); |
|
|
|
const pixels = decode(this.props.blurhash || this.state.blurhash, 32, 32); |
|
|
|
const outputImageData = new ImageData(pixels, 32, 32); |
|
|
|
|
|
|
|
context.putImageData(outputImageData, 0, 0); |
|
|
|
} |
|
|
|
|
|
|
|
componentWillUnmount () { |
|
|
@ -410,31 +260,6 @@ class Audio extends React.PureComponent { |
|
|
|
this.analyser = analyser; |
|
|
|
} |
|
|
|
|
|
|
|
handlePosterLoad = image => { |
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
const context = canvas.getContext('2d'); |
|
|
|
|
|
|
|
canvas.width = image.width; |
|
|
|
canvas.height = image.height; |
|
|
|
|
|
|
|
context.drawImage(image, 0, 0); |
|
|
|
|
|
|
|
const inputImageData = context.getImageData(0, 0, image.width, image.height); |
|
|
|
const blurhash = encode(inputImageData.data, image.width, image.height, 4, 4); |
|
|
|
|
|
|
|
this.setState({ blurhash }); |
|
|
|
} |
|
|
|
|
|
|
|
_setColorScheme () { |
|
|
|
const blurhash = this.props.blurhash || this.state.blurhash; |
|
|
|
const averageColor = decodeRGB(decode83(blurhash.slice(2, 6))); |
|
|
|
|
|
|
|
this.setState({ |
|
|
|
color: adjustColor(averageColor), |
|
|
|
darkText: luma(averageColor) >= 165, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
handleDownload = () => { |
|
|
|
fetch(this.props.src).then(res => res.blob()).then(blob => { |
|
|
|
const element = document.createElement('a'); |
|
|
@ -594,8 +419,8 @@ class Audio extends React.PureComponent { |
|
|
|
|
|
|
|
const gradient = this.canvasContext.createLinearGradient(dx1, dy1, dx2, dy2); |
|
|
|
|
|
|
|
const mainColor = `rgb(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b})`; |
|
|
|
const lastColor = `rgba(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b}, 0)`; |
|
|
|
const mainColor = this._getAccentColor(); |
|
|
|
const lastColor = hex2rgba(mainColor, 0); |
|
|
|
|
|
|
|
gradient.addColorStop(0, mainColor); |
|
|
|
gradient.addColorStop(0.6, mainColor); |
|
|
@ -617,17 +442,25 @@ class Audio extends React.PureComponent { |
|
|
|
return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient())); |
|
|
|
} |
|
|
|
|
|
|
|
_getColor () { |
|
|
|
return `rgb(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b})`; |
|
|
|
_getAccentColor () { |
|
|
|
return this.props.accentColor || '#ffffff'; |
|
|
|
} |
|
|
|
|
|
|
|
_getBackgroundColor () { |
|
|
|
return this.props.backgroundColor || '#000000'; |
|
|
|
} |
|
|
|
|
|
|
|
_getForegroundColor () { |
|
|
|
return this.props.foregroundColor || '#ffffff'; |
|
|
|
} |
|
|
|
|
|
|
|
render () { |
|
|
|
const { src, intl, alt, editable } = this.props; |
|
|
|
const { paused, muted, volume, currentTime, duration, buffer, darkText, dragging } = this.state; |
|
|
|
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state; |
|
|
|
const progress = (currentTime / duration) * 100; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className={classNames('audio-player', { editable, 'with-light-background': darkText })} ref={this.setPlayerRef} style={{ width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> |
|
|
|
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> |
|
|
|
<audio |
|
|
|
src={src} |
|
|
|
ref={this.setAudioRef} |
|
|
@ -639,24 +472,15 @@ class Audio extends React.PureComponent { |
|
|
|
/> |
|
|
|
|
|
|
|
<canvas |
|
|
|
className='audio-player__background' |
|
|
|
onClick={this.togglePlay} |
|
|
|
width='32' |
|
|
|
height='32' |
|
|
|
style={{ width: this.state.width, height: this.state.height, position: 'absolute', top: 0, left: 0 }} |
|
|
|
ref={this.setBlurhashCanvasRef} |
|
|
|
aria-label={alt} |
|
|
|
title={alt} |
|
|
|
role='button' |
|
|
|
tabIndex='0' |
|
|
|
/> |
|
|
|
|
|
|
|
<canvas |
|
|
|
className='audio-player__canvas' |
|
|
|
width={this.state.width} |
|
|
|
height={this.state.height} |
|
|
|
style={{ width: '100%', position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} |
|
|
|
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }} |
|
|
|
ref={this.setCanvasRef} |
|
|
|
onClick={this.togglePlay} |
|
|
|
title={alt} |
|
|
|
aria-label={alt} |
|
|
|
/> |
|
|
|
|
|
|
|
<img |
|
|
@ -669,12 +493,12 @@ class Audio extends React.PureComponent { |
|
|
|
|
|
|
|
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> |
|
|
|
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} /> |
|
|
|
<div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getColor() }} /> |
|
|
|
<div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getAccentColor() }} /> |
|
|
|
|
|
|
|
<span |
|
|
|
className={classNames('video-player__seek__handle', { active: dragging })} |
|
|
|
tabIndex='0' |
|
|
|
style={{ left: `${progress}%`, backgroundColor: this._getColor() }} |
|
|
|
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
@ -685,12 +509,12 @@ class Audio extends React.PureComponent { |
|
|
|
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> |
|
|
|
|
|
|
|
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}> |
|
|
|
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getColor() }} /> |
|
|
|
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} /> |
|
|
|
|
|
|
|
<span |
|
|
|
className={classNames('video-player__volume__handle')} |
|
|
|
tabIndex='0' |
|
|
|
style={{ left: `${volume * 100}%`, backgroundColor: this._getColor() }} |
|
|
|
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|