834 lines
23 KiB
JavaScript
834 lines
23 KiB
JavaScript
import React from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import { connect } from 'react-redux'
|
|
import ImmutablePropTypes from 'react-immutable-proptypes'
|
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
|
import { defineMessages, injectIntl } from 'react-intl'
|
|
import { is } from 'immutable'
|
|
import throttle from 'lodash.throttle'
|
|
import { decode } from 'blurhash'
|
|
import { isFullscreen, requestFullscreen, exitFullscreen } from '../utils/fullscreen'
|
|
import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'
|
|
import {
|
|
openPopover,
|
|
} from '../actions/popover'
|
|
import { displayMedia } from '../initial_state'
|
|
import {
|
|
CX,
|
|
POPOVER_VIDEO_STATS,
|
|
BREAKPOINT_EXTRA_SMALL,
|
|
} from '../constants'
|
|
import Responsive from '../features/ui/util/responsive_component'
|
|
import Button from './button'
|
|
import Icon from './icon'
|
|
import SensitiveMediaItem from './sensitive_media_item'
|
|
import Text from './text'
|
|
|
|
// check every 100 ms for buffer
|
|
const checkInterval = 100
|
|
const FIXED_VAR = 6
|
|
|
|
const formatTime = (secondsNum) => {
|
|
if (isNaN(secondsNum)) secondsNum = 0
|
|
|
|
let hours = Math.floor(secondsNum / 3600)
|
|
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60)
|
|
let seconds = Math.floor(secondsNum) - (hours * 3600) - (minutes * 60)
|
|
|
|
if (hours < 10) hours = '0' + hours
|
|
if (minutes < 10) minutes = '0' + minutes
|
|
if (seconds < 10) seconds = '0' + seconds
|
|
|
|
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`
|
|
}
|
|
|
|
export const findElementPosition = (el) => {
|
|
let box
|
|
|
|
if (el.getBoundingClientRect && el.parentNode) {
|
|
box = el.getBoundingClientRect()
|
|
}
|
|
|
|
if (!box) {
|
|
return {
|
|
left: 0,
|
|
top: 0,
|
|
}
|
|
}
|
|
|
|
const docEl = document.documentElement
|
|
const body = document.body
|
|
|
|
const clientLeft = docEl.clientLeft || body.clientLeft || 0
|
|
const scrollLeft = window.pageXOffset || body.scrollLeft
|
|
const left = (box.left + scrollLeft) - clientLeft
|
|
|
|
const clientTop = docEl.clientTop || body.clientTop || 0
|
|
const scrollTop = window.pageYOffset || body.scrollTop
|
|
const top = (box.top + scrollTop) - clientTop
|
|
|
|
return {
|
|
left: Math.round(left),
|
|
top: Math.round(top),
|
|
}
|
|
}
|
|
|
|
export const getPointerPosition = (el, event) => {
|
|
const position = {}
|
|
const box = findElementPosition(el)
|
|
const boxW = el.offsetWidth
|
|
const boxH = el.offsetHeight
|
|
const boxY = box.top
|
|
const boxX = box.left
|
|
|
|
let pageY = event.pageY
|
|
let pageX = event.pageX
|
|
|
|
if (event.changedTouches) {
|
|
pageX = event.changedTouches[0].pageX
|
|
pageY = event.changedTouches[0].pageY
|
|
}
|
|
|
|
position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH))
|
|
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW))
|
|
|
|
return position
|
|
}
|
|
|
|
class Video extends ImmutablePureComponent {
|
|
|
|
state = {
|
|
currentTime: 0,
|
|
duration: 0,
|
|
volume: 0.5,
|
|
paused: true,
|
|
dragging: false,
|
|
draggingVolume: false,
|
|
containerWidth: this.props.width,
|
|
fullscreen: false,
|
|
hovered: false,
|
|
muted: false,
|
|
hoveringVolumeButton: false,
|
|
hoveringVolumeControl: false,
|
|
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
|
pipAvailable: true,
|
|
isBuffering: false,
|
|
}
|
|
|
|
bufferCheckInterval = null
|
|
lastPlayPos = 0
|
|
volHeight = 100
|
|
volOffset = 13
|
|
|
|
componentDidMount() {
|
|
const { meta, blurhash } = this.props
|
|
|
|
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true)
|
|
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true)
|
|
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true)
|
|
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true)
|
|
|
|
if (meta) {
|
|
this.setState({ duration: parseInt(meta.get('duration')) })
|
|
}
|
|
|
|
if ('pictureInPictureEnabled' in document) {
|
|
this.setState({ pipAvailable: true })
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true)
|
|
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true)
|
|
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true)
|
|
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true)
|
|
|
|
clearInterval(this.bufferCheckInterval)
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
|
this.setState({ revealed: nextProps.visible })
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
if (prevState.revealed && !this.state.revealed && this.video) {
|
|
this.video.pause()
|
|
}
|
|
}
|
|
|
|
checkBuffering = () => {
|
|
const { isBuffering, paused } = this.state
|
|
if (!this.video) {
|
|
this.handlePause()
|
|
return
|
|
}
|
|
const { currentTime } = this.video
|
|
|
|
// Checking offset should be at most the check interval but allow for some margin
|
|
let offset = (checkInterval - 30) / 1000
|
|
|
|
if (!isBuffering && currentTime < (this.lastPlayPos + offset) && !paused) {
|
|
// If no buffering is currently detected, and the position does not seem to increase
|
|
// and the player isn't manually paused...
|
|
this.setState({ isBuffering: true })
|
|
} else if (isBuffering && currentTime > (this.lastPlayPos + offset) && !paused) {
|
|
// If we were buffering but the player has advanced, then there is no buffering
|
|
this.setState({ isBuffering: false })
|
|
}
|
|
|
|
this.lastPlayPos = currentTime
|
|
}
|
|
|
|
volHandleOffset = (v) => {
|
|
const offset = v * this.volHeight + this.volOffset
|
|
return (offset > 110) ? 110 : offset
|
|
}
|
|
|
|
setPlayerRef = (n) => {
|
|
this.player = n
|
|
|
|
if (n) {
|
|
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth)
|
|
this.setState({
|
|
containerWidth: n.offsetWidth,
|
|
})
|
|
}
|
|
}
|
|
|
|
setVideoRef = (n) => {
|
|
this.video = n
|
|
|
|
if (this.video) {
|
|
const { volume, muted } = this.video
|
|
this.setState({
|
|
volume,
|
|
muted,
|
|
})
|
|
}
|
|
}
|
|
|
|
setSeekRef = (n) => {
|
|
this.seek = n
|
|
}
|
|
|
|
setVolumeRef = (n) => {
|
|
this.volume = n
|
|
}
|
|
|
|
setSettingsBtnRef = (n) => {
|
|
this.settingsBtn = n
|
|
}
|
|
|
|
handleClickRoot = (e) => e.stopPropagation()
|
|
|
|
handlePlay = () => {
|
|
this.setState({ paused: false })
|
|
|
|
this.bufferCheckInterval = setInterval(this.checkBuffering, checkInterval)
|
|
}
|
|
|
|
handlePause = () => {
|
|
this.setState({
|
|
paused: true,
|
|
isBuffering: false,
|
|
})
|
|
|
|
clearInterval(this.bufferCheckInterval)
|
|
}
|
|
|
|
handleTimeUpdate = () => {
|
|
const { currentTime, duration } = this.video
|
|
this.setState({
|
|
currentTime: currentTime.toFixed(FIXED_VAR),
|
|
duration: duration.toFixed(FIXED_VAR),
|
|
})
|
|
}
|
|
|
|
handleVolumeMouseDown = (e) => {
|
|
document.addEventListener('mousemove', this.handleMouseVolSlide, true)
|
|
document.addEventListener('mouseup', this.handleVolumeMouseUp, true)
|
|
document.addEventListener('touchmove', this.handleMouseVolSlide, true)
|
|
document.addEventListener('touchend', this.handleVolumeMouseUp, true)
|
|
|
|
this.handleMouseVolSlide(e)
|
|
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
this.setState({ draggingVolume: true })
|
|
}
|
|
|
|
handleVolumeMouseUp = () => {
|
|
this.handleMouseLeaveVolumeControl()
|
|
|
|
document.removeEventListener('mousemove', this.handleMouseVolSlide, true)
|
|
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true)
|
|
document.removeEventListener('touchmove', this.handleMouseVolSlide, true)
|
|
document.removeEventListener('touchend', this.handleVolumeMouseUp, true)
|
|
|
|
this.setState({ draggingVolume: false })
|
|
}
|
|
|
|
handleMouseVolSlide = throttle((e) => {
|
|
const rect = this.volume.getBoundingClientRect()
|
|
const y = 1 - ((e.clientY - rect.top) / this.volHeight)
|
|
|
|
if (!isNaN(y)) {
|
|
const slideamt = y
|
|
if (y > 1) {
|
|
slideamt = 1
|
|
} else if (y < 0) {
|
|
slideamt = 0
|
|
}
|
|
this.video.volume = slideamt
|
|
this.setState({ volume: slideamt })
|
|
}
|
|
}, 60)
|
|
|
|
handleMouseDown = (e) => {
|
|
document.addEventListener('mousemove', this.handleMouseMove, true)
|
|
document.addEventListener('mouseup', this.handleMouseUp, true)
|
|
document.addEventListener('touchmove', this.handleMouseMove, true)
|
|
document.addEventListener('touchend', this.handleMouseUp, true)
|
|
|
|
this.setState({ dragging: true })
|
|
this.video.pause()
|
|
this.handleMouseMove(e)
|
|
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
|
|
handleMouseUp = () => {
|
|
document.removeEventListener('mousemove', this.handleMouseMove, true)
|
|
document.removeEventListener('mouseup', this.handleMouseUp, true)
|
|
document.removeEventListener('touchmove', this.handleMouseMove, true)
|
|
document.removeEventListener('touchend', this.handleMouseUp, true)
|
|
|
|
this.setState({ dragging: false })
|
|
this.video.play()
|
|
}
|
|
|
|
handleMouseMove = throttle(e => {
|
|
const { x } = getPointerPosition(this.seek, e)
|
|
const currentTime = parseFloat(this.video.duration * x).toFixed(FIXED_VAR)
|
|
|
|
if (!isNaN(currentTime)) {
|
|
this.video.currentTime = currentTime
|
|
this.setState({ currentTime })
|
|
}
|
|
}, 60)
|
|
|
|
togglePlay = () => {
|
|
if (this.state.paused) {
|
|
this.video.play()
|
|
} else {
|
|
this.video.pause()
|
|
}
|
|
}
|
|
|
|
toggleFullscreen = () => {
|
|
if (isFullscreen()) {
|
|
exitFullscreen()
|
|
} else {
|
|
requestFullscreen(this.player)
|
|
}
|
|
}
|
|
|
|
togglePip = () => {
|
|
try {
|
|
if (this.video !== document.pictureInPictureElement) {
|
|
if (this.state.paused) {
|
|
this.video.play()
|
|
}
|
|
setTimeout(() => { // : hack :
|
|
this.video.requestPictureInPicture()
|
|
}, 500)
|
|
} else {
|
|
document.exitPictureInPicture()
|
|
}
|
|
} catch(e) {
|
|
//
|
|
}
|
|
}
|
|
|
|
handleFullscreenChange = () => {
|
|
this.setState({ fullscreen: isFullscreen() })
|
|
}
|
|
|
|
handleMouseEnter = () => {
|
|
this.setState({ hovered: true })
|
|
}
|
|
|
|
handleMouseLeave = () => {
|
|
this.setState({ hovered: false })
|
|
}
|
|
|
|
handleMouseEnterAudio = () => {
|
|
this.setState({ hoveringVolumeButton: true })
|
|
}
|
|
|
|
handleMouseLeaveAudio = throttle(() => {
|
|
this.setState({ hoveringVolumeButton: false })
|
|
}, 2500)
|
|
|
|
handleMouseEnterVolumeControl = () => {
|
|
this.setState({ hoveringVolumeControl: true })
|
|
}
|
|
|
|
handleMouseLeaveVolumeControl = throttle(() => {
|
|
if (!this.state.draggingVolume) {
|
|
this.setState({ hoveringVolumeControl: false })
|
|
}
|
|
}, 2500)
|
|
|
|
toggleMute = () => {
|
|
this.video.muted = !this.video.muted
|
|
this.setState({ muted: this.video.muted })
|
|
}
|
|
|
|
toggleReveal = () => {
|
|
if (this.props.onToggleVisibility) {
|
|
this.props.onToggleVisibility()
|
|
} else {
|
|
this.setState({ revealed: !this.state.revealed })
|
|
}
|
|
}
|
|
|
|
handleLoadedData = () => {
|
|
if (this.props.startTime) {
|
|
this.video.currentTime = this.props.startTime
|
|
this.video.play()
|
|
}
|
|
}
|
|
|
|
handleProgress = () => {
|
|
const { buffered, duration } = this.video
|
|
|
|
if (!buffered) return
|
|
if (buffered.length > 0) {
|
|
this.setState({
|
|
buffer: buffered.end(0) / duration * 100,
|
|
})
|
|
}
|
|
}
|
|
|
|
handleVolumeChange = () => {
|
|
const { volume, muted } = this.video
|
|
this.setState({
|
|
volume,
|
|
muted,
|
|
})
|
|
}
|
|
|
|
handleOnClickSettings = () => {
|
|
this.props.onOpenVideoStatsPopover(this.settingsBtn, this.props.meta)
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
preview,
|
|
src,
|
|
inline,
|
|
startTime,
|
|
intl,
|
|
alt,
|
|
detailed,
|
|
sensitive,
|
|
aspectRatio,
|
|
} = this.props
|
|
|
|
const {
|
|
containerWidth,
|
|
currentTime,
|
|
duration,
|
|
volume,
|
|
buffer,
|
|
dragging,
|
|
paused,
|
|
fullscreen,
|
|
hovered,
|
|
muted,
|
|
revealed,
|
|
hoveringVolumeButton,
|
|
hoveringVolumeControl,
|
|
pipAvailable,
|
|
isBuffering,
|
|
} = this.state
|
|
|
|
const progress = (currentTime / duration) * 100
|
|
|
|
const volumeHeight = (muted) ? 0 : volume * this.volHeight
|
|
const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume)
|
|
const playerStyle = {}
|
|
|
|
let { width, height } = this.props
|
|
|
|
if (inline && containerWidth) {
|
|
width = containerWidth
|
|
const minSize = containerWidth / (16 / 9)
|
|
|
|
if (isPanoramic(aspectRatio)) {
|
|
height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize)
|
|
} else if (isPortrait(aspectRatio)) {
|
|
height = Math.max(Math.floor(containerWidth / minimumAspectRatio), minSize)
|
|
} else {
|
|
height = Math.floor(containerWidth / aspectRatio)
|
|
}
|
|
|
|
playerStyle.height = height
|
|
}
|
|
|
|
let preload
|
|
|
|
if (startTime || fullscreen || dragging) {
|
|
preload = 'auto'
|
|
} else if (detailed) {
|
|
preload = 'metadata'
|
|
} else {
|
|
preload = 'none'
|
|
}
|
|
|
|
// className={classNames('video-player', {
|
|
// inactive: !revealed,
|
|
// detailed,
|
|
// inline: inline && !fullscreen,
|
|
// fullscreen
|
|
// })}
|
|
|
|
// : todo spoiler :
|
|
|
|
const mainContainerClasses = CX({
|
|
d: 1,
|
|
mt10: 1,
|
|
outlineNone: 1,
|
|
})
|
|
|
|
const seekHandleClasses = CX({
|
|
d: 1,
|
|
posAbs: 1,
|
|
circle: 1,
|
|
h20PX: 1,
|
|
w20PX: 1,
|
|
bgTransparent: 1,
|
|
mlNeg5PX: 1,
|
|
mr5: 1,
|
|
z3: 1,
|
|
aiCenter: 1,
|
|
jcCenter: 1,
|
|
videoEase: 1,
|
|
opacity0: !dragging,
|
|
opacity1: dragging || hovered,
|
|
})
|
|
|
|
const seekInnerHandleClasses = CX({
|
|
d: 1,
|
|
circle: 1,
|
|
h14PX: 1,
|
|
w14PX: 1,
|
|
bgBrand: 1,
|
|
boxShadow1: 1,
|
|
})
|
|
|
|
const progressClasses = CX({
|
|
d: 1,
|
|
radiusSmall: 1,
|
|
mt10: 1,
|
|
posAbs: 1,
|
|
h4PX: 1,
|
|
videoEase: 1,
|
|
})
|
|
|
|
const volumeControlClasses = CX({
|
|
d: 1,
|
|
posAbs: 1,
|
|
bgBlackOpaque: 1,
|
|
videoPlayerVolume: 1,
|
|
h122PX: 1,
|
|
circle: 1,
|
|
displayNone: !hoveringVolumeButton && !hoveringVolumeControl || !hovered,
|
|
})
|
|
|
|
const videoControlsBackgroundClasses = CX({
|
|
d: 1,
|
|
z2: 1,
|
|
px15: 1,
|
|
videoPlayerControlsBackground: 1,
|
|
posAbs: 1,
|
|
bottom0: 1,
|
|
right0: 1,
|
|
left0: 1,
|
|
displayNone: !hovered && !paused,
|
|
})
|
|
|
|
const overlayClasses = CX({
|
|
d: 1,
|
|
top50PC: 1,
|
|
left50PC: 1,
|
|
posAbs: 1,
|
|
z2: 1,
|
|
aiCenter: 1,
|
|
jcCenter: 1,
|
|
displayNone: !paused && !isBuffering,
|
|
})
|
|
|
|
if (!revealed && sensitive) {
|
|
return <SensitiveMediaItem onClick={this.toggleReveal} />
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={mainContainerClasses}
|
|
style={playerStyle}
|
|
ref={this.setPlayerRef}
|
|
onMouseEnter={this.handleMouseEnter}
|
|
onMouseLeave={this.handleMouseLeave}
|
|
onClick={this.handleClickRoot}
|
|
tabIndex={0}
|
|
>
|
|
|
|
<div className={overlayClasses} id='overlay'>
|
|
{
|
|
paused && !isBuffering &&
|
|
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
|
<button
|
|
onClick={this.togglePlay}
|
|
className={[_s.d, _s.outlineNone, _s.cursorPointer, _s.aiCenter, _s.jcCenter, _s.posAbs, _s.bgBlackOpaque, _s.circle, _s.h60PX, _s.w60PX].join(' ')}
|
|
>
|
|
<Icon id='play' size='24px' className={_s.cWhite} />
|
|
</button>
|
|
</Responsive>
|
|
}
|
|
{
|
|
!paused && true &&
|
|
<Icon id='loading' size='60px' className={[_s.d, _s.posAbs].join(' ')} />
|
|
}
|
|
</div>
|
|
|
|
<video
|
|
className={[_s.d, _s.h100PC, _s.w100PC, _s.outlineNone].join(' ')}
|
|
playsInline
|
|
ref={this.setVideoRef}
|
|
src={src}
|
|
poster={preview}
|
|
preload={preload}
|
|
role='button'
|
|
tabIndex='0'
|
|
aria-label={alt}
|
|
title={alt}
|
|
width={width}
|
|
height={height}
|
|
volume={volume}
|
|
onClick={this.togglePlay}
|
|
onPlay={this.handlePlay}
|
|
onPause={this.handlePause}
|
|
onTimeUpdate={this.handleTimeUpdate}
|
|
onLoadedData={this.handleLoadedData}
|
|
onProgress={this.handleProgress}
|
|
onVolumeChange={this.handleVolumeChange}
|
|
/>
|
|
|
|
{ /* <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
|
|
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
|
|
<span className='spoiler-button__overlay__label'>
|
|
{intl.formatMessage(sensitive ? messages.sensitive : messages.hidden)}
|
|
</span>
|
|
</button>
|
|
</div> */ }
|
|
|
|
<div
|
|
className={volumeControlClasses}
|
|
onMouseDown={this.handleVolumeMouseDown}
|
|
onMouseEnter={this.handleMouseEnterVolumeControl}
|
|
onMouseLeave={this.handleMouseLeaveVolumeControl}
|
|
ref={this.setVolumeRef}
|
|
>
|
|
<div
|
|
className={[_s.d, _s.radiusSmall, _s.my10, _s.posAbs, _s.w4PX, _s.ml10, _s.bgPrimaryOpaque].join(' ')}
|
|
style={{
|
|
height: '102px',
|
|
}}
|
|
/>
|
|
<div
|
|
className={[_s.d, _s.radiusSmall, _s.my10, _s.bottom0, _s.posAbs, _s.w4PX, _s.ml10, _s.bgPrimary].join(' ')}
|
|
style={{
|
|
height: `${volumeHeight}px`
|
|
}}
|
|
/>
|
|
<span
|
|
className={[_s.d, _s.cursorPointer, _s.posAbs, _s.circle, _s.px5, _s.boxShadow1, _s.mbNeg5PX, _s.py5, _s.bgPrimary, _s.z3].join(' ')}
|
|
tabIndex='0'
|
|
style={{
|
|
marginLeft: '7px',
|
|
bottom: `${volumeHandleLoc}px`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className={videoControlsBackgroundClasses}>
|
|
|
|
<div
|
|
className={[_s.d, _s.cursorPointer, _s.h22PX, _s.videoPlayerSeek].join(' ')}
|
|
onMouseDown={this.handleMouseDown}
|
|
ref={this.setSeekRef}
|
|
>
|
|
|
|
<div className={[progressClasses, _s.bgLoading, _s.w100PC].join(' ')} />
|
|
<div className={[progressClasses, _s.bgSubtle].join(' ')} style={{ width: `${buffer}%` }} />
|
|
<div className={[progressClasses, _s.bgBrand].join(' ')} style={{ width: `${progress}%` }} />
|
|
|
|
<span
|
|
className={seekHandleClasses}
|
|
tabIndex='0'
|
|
style={{
|
|
left: `${progress}%`
|
|
}}
|
|
>
|
|
<span className={seekInnerHandleClasses} />
|
|
</span>
|
|
</div>
|
|
|
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.pb5, _s.noSelect].join(' ')}>
|
|
<Button
|
|
isNarrow
|
|
backgroundColor='none'
|
|
aria-label={intl.formatMessage(paused ? messages.play : messages.pause)}
|
|
onClick={this.togglePlay}
|
|
icon={paused ? 'play' : 'pause'}
|
|
title={paused ? 'Play' : 'Pause'}
|
|
iconSize='16px'
|
|
iconClassName={_s.cWhite}
|
|
className={_s.pl0}
|
|
/>
|
|
|
|
<Button
|
|
isNarrow
|
|
backgroundColor='none'
|
|
type='button'
|
|
aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)}
|
|
onClick={this.toggleMute}
|
|
icon={muted ? 'audio-mute' : 'audio'}
|
|
iconSize='24px'
|
|
iconClassName={_s.cWhite}
|
|
className={[_s.px10, _s.mr10].join(' ')}
|
|
title='Volume'
|
|
onMouseEnter={this.handleMouseEnterAudio}
|
|
onMouseLeave={this.handleMouseLeaveAudio}
|
|
/>
|
|
|
|
<Text color='white' size='small'>
|
|
{formatTime(currentTime)}
|
|
/
|
|
{formatTime(duration)}
|
|
</Text>
|
|
|
|
<div className={[_s.d, _s.mlAuto, _s.flexRow, _s.aiCenter].join(' ')}>
|
|
<Button
|
|
isNarrow
|
|
backgroundColor='none'
|
|
aria-label={intl.formatMessage(messages.video_stats)}
|
|
onClick={this.handleOnClickSettings}
|
|
icon='cog'
|
|
iconSize='20px'
|
|
iconClassName={_s.cWhite}
|
|
className={[_s.px10, _s.pr0].join(' ')}
|
|
buttonRef={this.setSettingsBtnRef}
|
|
title='Video stats'
|
|
/>
|
|
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
|
{
|
|
pipAvailable &&
|
|
<Button
|
|
isNarrow
|
|
backgroundColor='none'
|
|
aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
|
|
onClick={this.togglePip}
|
|
icon='pip'
|
|
iconSize='20px'
|
|
iconClassName={_s.cWhite}
|
|
className={[_s.px10, _s.pr0].join(' ')}
|
|
title='Picture in Picture'
|
|
/>
|
|
}
|
|
</Responsive>
|
|
<Button
|
|
isNarrow
|
|
backgroundColor='none'
|
|
aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
|
|
onClick={this.toggleFullscreen}
|
|
icon={fullscreen ? 'minimize-fullscreen' : 'fullscreen'}
|
|
title={fullscreen ? 'Minimize fullscreen' : 'Fullscreen'}
|
|
iconSize='20px'
|
|
iconClassName={_s.cWhite}
|
|
className={[_s.px10, _s.pr0].join(' ')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{
|
|
revealed && sensitive &&
|
|
<div className={[_s.posAbs, _s.z2, _s.top0, _s.right0, _s.mt10, _s.mr10].join(' ')}>
|
|
<Button
|
|
title={intl.formatMessage(messages.toggle_visible)}
|
|
icon='hidden'
|
|
backgroundColor='black'
|
|
className={[_s.px10, _s.bgBlackOpaque_onHover].join(' ')}
|
|
onClick={this.toggleReveal}
|
|
/>
|
|
</div>
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
const messages = defineMessages({
|
|
play: { id: 'video.play', defaultMessage: 'Play' },
|
|
pause: { id: 'video.pause', defaultMessage: 'Pause' },
|
|
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
|
|
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
|
|
hide: { id: 'video.hide', defaultMessage: 'Hide video' },
|
|
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
|
|
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
|
sensitive: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
|
|
hidden: { id: 'status.media_hidden', defaultMessage: 'Media hidden' },
|
|
video_stats: { id: 'video.stats_label', defaultMessage: 'Video meta stats' },
|
|
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide media' },
|
|
})
|
|
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
onOpenVideoStatsPopover(targetRef, meta) {
|
|
dispatch(openPopover(POPOVER_VIDEO_STATS, {
|
|
targetRef,
|
|
meta,
|
|
position: 'top',
|
|
}))
|
|
}
|
|
})
|
|
|
|
Video.propTypes = {
|
|
preview: PropTypes.string,
|
|
src: PropTypes.string.isRequired,
|
|
alt: PropTypes.string,
|
|
width: PropTypes.number,
|
|
height: PropTypes.number,
|
|
sensitive: PropTypes.bool,
|
|
startTime: PropTypes.number,
|
|
detailed: PropTypes.bool,
|
|
inline: PropTypes.bool,
|
|
cacheWidth: PropTypes.func,
|
|
visible: PropTypes.bool,
|
|
onToggleVisibility: PropTypes.func,
|
|
intl: PropTypes.object.isRequired,
|
|
blurhash: PropTypes.string,
|
|
aspectRatio: PropTypes.number,
|
|
meta: ImmutablePropTypes.map,
|
|
onOpenVideoStatsPopover: PropTypes.func.isRequired,
|
|
}
|
|
|
|
export default injectIntl(connect(null, mapDispatchToProps)(Video)) |