You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

258 lines
7.6 KiB

7 years ago
7 years ago
  1. import ImmutablePropTypes from 'react-immutable-proptypes';
  2. import PropTypes from 'prop-types';
  3. import IconButton from './icon_button';
  4. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  5. import { isIOS } from '../is_mobile';
  6. const messages = defineMessages({
  7. toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
  8. toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
  9. expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
  10. expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
  11. });
  12. const videoStyle = {
  13. position: 'relative',
  14. zIndex: '1',
  15. width: '100%',
  16. height: '100%',
  17. objectFit: 'cover',
  18. top: '50%',
  19. transform: 'translateY(-50%)'
  20. };
  21. const muteStyle = {
  22. position: 'absolute',
  23. top: '4px',
  24. right: '4px',
  25. color: 'white',
  26. textShadow: "0px 1px 1px black, 1px 0px 1px black",
  27. opacity: '0.8',
  28. zIndex: '5'
  29. };
  30. const coverStyle = {
  31. marginTop: '8px',
  32. textAlign: 'center',
  33. height: '100%',
  34. cursor: 'pointer',
  35. display: 'flex',
  36. alignItems: 'center',
  37. justifyContent: 'center',
  38. flexDirection: 'column',
  39. position: 'relative'
  40. };
  41. const spoilerSpanStyle = {
  42. display: 'block',
  43. fontSize: '14px'
  44. };
  45. const spoilerSubSpanStyle = {
  46. display: 'block',
  47. fontSize: '11px',
  48. fontWeight: '500'
  49. };
  50. const spoilerButtonStyle = {
  51. position: 'absolute',
  52. top: '4px',
  53. left: '4px',
  54. color: 'white',
  55. textShadow: "0px 1px 1px black, 1px 0px 1px black",
  56. zIndex: '100'
  57. };
  58. const expandButtonStyle = {
  59. position: 'absolute',
  60. bottom: '4px',
  61. right: '4px',
  62. color: 'white',
  63. textShadow: "0px 1px 1px black, 1px 0px 1px black",
  64. zIndex: '100'
  65. };
  66. class VideoPlayer extends React.PureComponent {
  67. constructor (props, context) {
  68. super(props, context);
  69. this.state = {
  70. visible: !this.props.sensitive,
  71. preview: true,
  72. muted: true,
  73. hasAudio: true,
  74. videoError: false
  75. };
  76. this.handleClick = this.handleClick.bind(this);
  77. this.handleVideoClick = this.handleVideoClick.bind(this);
  78. this.handleOpen = this.handleOpen.bind(this);
  79. this.handleVisibility = this.handleVisibility.bind(this);
  80. this.handleExpand = this.handleExpand.bind(this);
  81. this.setRef = this.setRef.bind(this);
  82. this.handleLoadedData = this.handleLoadedData.bind(this);
  83. this.handleVideoError = this.handleVideoError.bind(this);
  84. }
  85. handleClick () {
  86. this.setState({ muted: !this.state.muted });
  87. }
  88. handleVideoClick (e) {
  89. e.stopPropagation();
  90. const node = ReactDOM.findDOMNode(this).querySelector('video');
  91. if (node.paused) {
  92. node.play();
  93. } else {
  94. node.pause();
  95. }
  96. }
  97. handleOpen () {
  98. this.setState({ preview: !this.state.preview });
  99. }
  100. handleVisibility () {
  101. this.setState({
  102. visible: !this.state.visible,
  103. preview: true
  104. });
  105. }
  106. handleExpand () {
  107. this.video.pause();
  108. this.props.onOpenVideo(this.props.media, this.video.currentTime);
  109. }
  110. setRef (c) {
  111. this.video = c;
  112. }
  113. handleLoadedData () {
  114. if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
  115. this.setState({ hasAudio: false });
  116. }
  117. }
  118. handleVideoError () {
  119. this.setState({ videoError: true });
  120. }
  121. componentDidMount () {
  122. if (!this.video) {
  123. return;
  124. }
  125. this.video.addEventListener('loadeddata', this.handleLoadedData);
  126. this.video.addEventListener('error', this.handleVideoError);
  127. }
  128. componentDidUpdate () {
  129. if (!this.video) {
  130. return;
  131. }
  132. this.video.addEventListener('loadeddata', this.handleLoadedData);
  133. this.video.addEventListener('error', this.handleVideoError);
  134. }
  135. componentWillUnmount () {
  136. if (!this.video) {
  137. return;
  138. }
  139. this.video.removeEventListener('loadeddata', this.handleLoadedData);
  140. this.video.removeEventListener('error', this.handleVideoError);
  141. }
  142. render () {
  143. const { media, intl, width, height, sensitive, autoplay } = this.props;
  144. let spoilerButton = (
  145. <div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
  146. <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
  147. </div>
  148. );
  149. let expandButton = (
  150. <div style={expandButtonStyle} >
  151. <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
  152. </div>
  153. );
  154. let muteButton = '';
  155. if (this.state.hasAudio) {
  156. muteButton = (
  157. <div style={muteStyle}>
  158. <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
  159. </div>
  160. );
  161. }
  162. if (!this.state.visible) {
  163. if (sensitive) {
  164. return (
  165. <div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
  166. {spoilerButton}
  167. <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
  168. <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
  169. </div>
  170. );
  171. } else {
  172. return (
  173. <div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
  174. {spoilerButton}
  175. <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
  176. <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
  177. </div>
  178. );
  179. }
  180. }
  181. if (this.state.preview && !autoplay) {
  182. return (
  183. <div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
  184. {spoilerButton}
  185. <div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
  186. </div>
  187. );
  188. }
  189. if (this.state.videoError) {
  190. return (
  191. <div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
  192. <span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
  193. </div>
  194. );
  195. }
  196. return (
  197. <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
  198. {spoilerButton}
  199. {muteButton}
  200. {expandButton}
  201. <video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
  202. </div>
  203. );
  204. }
  205. }
  206. VideoPlayer.propTypes = {
  207. media: ImmutablePropTypes.map.isRequired,
  208. width: PropTypes.number,
  209. height: PropTypes.number,
  210. sensitive: PropTypes.bool,
  211. intl: PropTypes.object.isRequired,
  212. autoplay: PropTypes.bool,
  213. onOpenVideo: PropTypes.func.isRequired
  214. };
  215. VideoPlayer.defaultProps = {
  216. width: 239,
  217. height: 110
  218. };
  219. export default injectIntl(VideoPlayer);