lbry-react-native/src/component/mediaPlayer/view.js
2019-08-15 10:03:12 +01:00

510 lines
15 KiB
JavaScript

import React from 'react';
import { Lbry } from 'lbry-redux';
import {
AppState,
ActivityIndicator,
DeviceEventEmitter,
NativeModules,
PanResponder,
Text,
View,
ScrollView,
TouchableOpacity,
} from 'react-native';
import Colors from 'styles/colors';
import FastImage from 'react-native-fast-image';
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome5';
import FileItemMedia from 'component/fileItemMedia';
import mediaPlayerStyle from 'styles/mediaPlayer';
const positionSaveInterval = 10;
class MediaPlayer extends React.PureComponent {
static ControlsTimeout = 3000;
seekResponder = null;
seekerWidth = 0;
trackingOffset = 0;
tracking = null;
video = null;
constructor(props) {
super(props);
this.state = {
buffering: false,
backgroundPlayEnabled: false,
autoPaused: false,
rate: 1,
volume: 1,
muted: false,
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: !props.autoPlay,
fullscreenMode: false,
areControlsVisible: true,
controlsTimeout: -1,
seekerOffset: 0,
seekerPosition: 0,
firstPlay: true,
seekTimeout: -1,
};
}
formatTime(time) {
let str = '';
let minutes = 0,
hours = 0,
seconds = parseInt(time, 10);
if (seconds > 60) {
minutes = parseInt(seconds / 60, 10);
seconds = seconds % 60;
if (minutes > 60) {
hours = parseInt(minutes / 60, 10);
minutes = minutes % 60;
}
str = (hours > 0 ? this.pad(hours) + ':' : '') + this.pad(minutes) + ':' + this.pad(seconds);
} else {
str = '00:' + this.pad(seconds);
}
return str;
}
pad(value) {
if (value < 10) {
return '0' + String(value);
}
return value;
}
onLoad = data => {
this.setState({
duration: data.duration,
});
const { position } = this.props;
if (!isNaN(parseFloat(position)) && position > 0) {
this.video.seek(position);
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
}
if (this.props.onMediaLoaded) {
this.props.onMediaLoaded();
}
};
onProgress = data => {
const { savePosition, claim } = this.props;
this.setState({ buffering: false, currentTime: data.currentTime });
if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
const { claim_id: claimId, txid, nout } = claim;
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
}
if (!this.state.seeking) {
this.setSeekerPosition(this.calculateSeekerPosition());
}
if (this.state.firstPlay) {
if (this.props.onPlaybackStarted) {
this.props.onPlaybackStarted();
}
this.setState({ firstPlay: false });
this.hidePlayerControls();
}
};
clearControlsTimeout = () => {
if (this.state.controlsTimeout > -1) {
clearTimeout(this.state.controlsTimeout);
}
};
showPlayerControls = () => {
this.clearControlsTimeout();
if (!this.state.areControlsVisible) {
this.setState({ areControlsVisible: true });
}
this.hidePlayerControls();
};
manualHidePlayerControls = () => {
this.clearControlsTimeout();
this.setState({ areControlsVisible: false });
};
hidePlayerControls() {
const player = this;
let timeout = setTimeout(() => {
player.setState({ areControlsVisible: false });
}, MediaPlayer.ControlsTimeout);
player.setState({ controlsTimeout: timeout });
}
togglePlayerControls = () => {
const { setPlayerVisible, isPlayerVisible } = this.props;
if (!isPlayerVisible) {
setPlayerVisible();
}
if (this.state.areControlsVisible) {
this.manualHidePlayerControls();
} else {
this.showPlayerControls();
}
};
togglePlay = () => {
this.showPlayerControls();
this.setState({ paused: !this.state.paused }, this.handlePausedState);
};
handlePausedState = () => {
if (!this.state.paused) {
// onProgress will automatically clear this, so it's fine
this.setState({ buffering: true });
}
};
toggleFullscreenMode = () => {
this.showPlayerControls();
const { onFullscreenToggled } = this.props;
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
if (onFullscreenToggled) {
onFullscreenToggled(this.state.fullscreenMode);
}
});
};
onEnd = () => {
this.setState({ paused: true });
if (this.props.onPlaybackFinished) {
this.props.onPlaybackFinished();
}
this.video.seek(0);
};
setSeekerPosition(position = 0) {
position = this.checkSeekerPosition(position);
this.setState({ seekerPosition: position });
if (!this.state.seeking) {
this.setState({ seekerOffset: position });
}
}
checkSeekerPosition(val = 0) {
if (val < 0) {
val = 0;
} else if (val >= this.seekerWidth) {
return this.seekerWidth;
}
return val;
}
seekTo(time = 0) {
if (time > this.state.duration) {
return;
}
this.video.seek(time);
this.setState({ currentTime: time });
}
initSeeker() {
this.seekResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.clearControlsTimeout();
if (this.state.seekTimeout > 0) {
clearTimeout(this.state.seekTimeout);
}
this.setState({ seeking: true });
},
onPanResponderMove: (evt, gestureState) => {
const position = this.state.seekerOffset + gestureState.dx;
this.setSeekerPosition(position);
},
onPanResponderRelease: (evt, gestureState) => {
const time = this.getCurrentTimeForSeekerPosition();
if (time >= this.state.duration) {
this.setState({ paused: true }, this.handlePausedState);
this.onEnd();
} else {
this.seekTo(time);
this.setState({
seekTimeout: setTimeout(() => {
this.setState({ seeking: false });
}, 100),
});
}
this.hidePlayerControls();
},
});
}
getTrackingOffset() {
return this.state.fullscreenMode ? this.trackingOffset : 0;
}
getCurrentTimeForSeekerPosition() {
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
}
calculateSeekerPosition() {
return this.seekerWidth * this.getCurrentTimePercentage();
}
getCurrentTimePercentage() {
if (this.state.currentTime > 0) {
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
}
return 0;
}
componentWillMount() {
this.initSeeker();
}
componentWillReceiveProps(nextProps) {
const { isPlayerVisible } = nextProps;
if (!isPlayerVisible && !this.state.backgroundPlayEnabled) {
// force pause if the player is not visible and background play is not enabled
this.setState({ paused: true });
}
}
componentDidMount() {
const { assignPlayer, backgroundPlayEnabled } = this.props;
if (assignPlayer) {
assignPlayer(this);
}
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
this.setSeekerPosition(this.calculateSeekerPosition());
AppState.addEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
this.clearControlsTimeout();
this.setState({ paused: true, fullscreenMode: false });
const { onFullscreenToggled } = this.props;
if (onFullscreenToggled) {
onFullscreenToggled(false);
}
}
handleAppStateChange = () => {
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
if (!this.state.backgroundPlayEnabled && !this.state.paused) {
this.setState({ paused: true, autoPaused: true });
}
}
if (AppState.currentState && AppState.currentState.match(/active/)) {
if (!this.state.backgroundPlayEnabled && this.state.autoPaused) {
this.setState({ paused: false, autoPaused: false });
}
}
};
onBuffer = () => {
if (!this.state.paused) {
this.setState({ buffering: true }, () => this.manualHidePlayerControls());
}
};
play = () => {
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
};
pause = () => {
this.setState({ paused: true }, this.updateBackgroundMediaNotification);
};
updateBackgroundMediaNotification = () => {
this.handlePausedState();
const { backgroundPlayEnabled } = this.props;
if (backgroundPlayEnabled) {
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
const { title, channel, uri } = window.currentMediaInfo;
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, this.state.paused);
}
}
};
renderPlayerControls() {
const { onBackButtonPressed } = this.props;
if (this.state.areControlsVisible) {
return (
<View style={mediaPlayerStyle.playerControlsContainer}>
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
<Icon name={'arrow-left'} size={18} style={mediaPlayerStyle.backButtonIcon} />
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton} onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
{this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
{!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
</TouchableOpacity>
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
<Text style={mediaPlayerStyle.totalDuration}>{this.formatTime(this.state.duration)}</Text>
</View>
);
}
return null;
}
onSeekerTouchAreaPressed = evt => {
if (evt && evt.nativeEvent) {
const newSeekerPosition = evt.nativeEvent.locationX;
if (!isNaN(newSeekerPosition)) {
const time = this.state.duration * (newSeekerPosition / this.seekerWidth);
this.setSeekerPosition(newSeekerPosition);
this.seekTo(time);
}
}
};
onTrackingLayout = evt => {
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
this.setSeekerPosition(this.calculateSeekerPosition());
};
render() {
const { onLayout, source, style, thumbnail } = this.props;
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
const remainingWidth = this.seekerWidth - completedWidth;
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
const trackingStyle = [
mediaPlayerStyle.trackingControls,
this.state.fullscreenMode
? mediaPlayerStyle.fullscreenTrackingControls
: mediaPlayerStyle.containedTrackingControls,
];
const seekerCircleStyle = [this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle];
if (!this.state.seeking) {
seekerCircleStyle.push(
this.state.fullscreenMode ? mediaPlayerStyle.seekerCircleTopFs : mediaPlayerStyle.seekerCircleTop
);
}
return (
<View style={styles} onLayout={onLayout}>
<Video
source={{ uri: source }}
bufferConfig={{
minBufferMs: 15000,
maxBufferMs: 60000,
bufferForPlaybackMs: 5000,
bufferForPlaybackAfterRebufferMs: 5000,
}}
ref={ref => {
this.video = ref;
}}
resizeMode={this.state.resizeMode}
playInBackground={this.state.backgroundPlayEnabled}
style={mediaPlayerStyle.player}
rate={this.state.rate}
volume={this.state.volume}
paused={this.state.paused}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={this.onEnd}
onError={this.onError}
minLoadRetryCount={999}
/>
{this.state.firstPlay && thumbnail && (
<FastImage
source={{ uri: thumbnail }}
resizeMode={FastImage.resizeMode.cover}
style={mediaPlayerStyle.playerThumbnail}
/>
)}
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.togglePlayerControls}>
{this.renderPlayerControls()}
</TouchableOpacity>
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) && (
<View style={trackingStyle} onLayout={this.onTrackingLayout}>
<View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
</View>
</View>
)}
{this.state.buffering && (
<View style={mediaPlayerStyle.loadingContainer}>
<ActivityIndicator color={Colors.NextLbryGreen} size="large" />
</View>
)}
{this.state.areControlsVisible && (
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
<View
style={[
mediaPlayerStyle.seekerHandle,
this.state.fullscreenMode ? mediaPlayerStyle.seekerHandleFs : mediaPlayerStyle.seekerHandleContained,
{ left: this.state.seekerPosition },
]}
{...this.seekResponder.panHandlers}
>
<View style={seekerCircleStyle} />
</View>
<TouchableOpacity
style={[
mediaPlayerStyle.seekerTouchArea,
this.state.fullscreenMode
? mediaPlayerStyle.seekerTouchAreaFs
: mediaPlayerStyle.seekerTouchAreaContained,
]}
onPress={this.onSeekerTouchAreaPressed}
/>
</View>
)}
</View>
);
}
}
export default MediaPlayer;