Video fullscreen switch (#132)

* file page download progress display and media loading / playback tweaks
* Moved MediaPlayer component to be a child of the top-level view
* hide tracking controls when player controls are hidden in fullscreen mode
This commit is contained in:
akinwale 2018-05-19 16:11:59 +01:00 committed by GitHub
parent f850be1eca
commit ceeef6324e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 70 deletions

View file

@ -20,23 +20,30 @@ class MediaPlayer extends React.PureComponent {
seekerWidth = 0; seekerWidth = 0;
trackingOffset = 0;
tracking = null;
video = null; video = null;
state = { constructor(props) {
rate: 1, super(props);
volume: 1, this.state = {
muted: false, rate: 1,
resizeMode: 'stretch', volume: 1,
duration: 0.0, muted: false,
currentTime: 0.0, resizeMode: 'stretch',
paused: true, duration: 0.0,
fullscreenMode: false, currentTime: 0.0,
areControlsVisible: true, paused: true,
controlsTimeout: -1, fullscreenMode: false,
seekerOffset: 0, areControlsVisible: true,
seekerPosition: 0, controlsTimeout: -1,
firstPlay: true seekerOffset: 0,
}; seekerPosition: 0,
firstPlay: true
};
}
formatTime(time) { formatTime(time) {
let str = ''; let str = '';
@ -144,10 +151,11 @@ class MediaPlayer extends React.PureComponent {
} }
checkSeekerPosition(val = 0) { checkSeekerPosition(val = 0) {
if (val < 0) { const offset = this.getTrackingOffset();
val = 0; if (val < offset) {
} else if (val >= this.seekerWidth) { val = offset;
return this.seekerWidth; } else if (val >= (offset + this.seekerWidth)) {
return offset + this.seekerWidth;
} }
return val; return val;
@ -190,11 +198,18 @@ class MediaPlayer extends React.PureComponent {
}); });
} }
getTrackingOffset() {
return this.state.fullscreenMode ? this.trackingOffset : 0;
}
getCurrentTimeForSeekerPosition() { getCurrentTimeForSeekerPosition() {
return this.state.duration * (this.state.seekerPosition / this.seekerWidth); return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
} }
calculateSeekerPosition() { calculateSeekerPosition() {
if (this.state.fullscreenMode) {
return this.getTrackingOffset() + (this.seekerWidth * this.getCurrentTimePercentage());
}
return this.seekerWidth * this.getCurrentTimePercentage(); return this.seekerWidth * this.getCurrentTimePercentage();
} }
@ -209,6 +224,10 @@ class MediaPlayer extends React.PureComponent {
this.initSeeker(); this.initSeeker();
} }
componentDidMount() {
}
componentWillUnmount() { componentWillUnmount() {
this.clearControlsTimeout(); this.clearControlsTimeout();
this.setState({ paused: true, fullscreenMode: false }); this.setState({ paused: true, fullscreenMode: false });
@ -243,13 +262,23 @@ class MediaPlayer extends React.PureComponent {
} }
render() { render() {
const { backgroundPlayEnabled, fileInfo, thumbnail, style, fullScreenStyle } = this.props; const { backgroundPlayEnabled, fileInfo, thumbnail, style } = this.props;
const flexCompleted = this.getCurrentTimePercentage() * 100; const flexCompleted = this.getCurrentTimePercentage() * 100;
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100; const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
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];
return ( return (
<View style={[style, mediaPlayerStyle.container]}> <View style={styles}>
<View style={mediaPlayerStyle.playerBackground} />
<Video source={{ uri: 'file:///' + fileInfo.download_path }} <Video source={{ uri: 'file:///' + fileInfo.download_path }}
ref={(ref: Video) => { this.video = ref }} ref={(ref: Video) => { this.video = ref }}
resizeMode={this.state.resizeMode} resizeMode={this.state.resizeMode}
@ -267,12 +296,16 @@ class MediaPlayer extends React.PureComponent {
{this.renderPlayerControls()} {this.renderPlayerControls()}
</TouchableOpacity> </TouchableOpacity>
<View style={mediaPlayerStyle.trackingControls}> {(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
<View style={mediaPlayerStyle.progress} onLayout={(evt) => this.seekerWidth = evt.nativeEvent.layout.width}> <View style={trackingStyle} onLayout={(evt) => {
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
}}>
<View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { flex: flexCompleted }]} /> <View style={[mediaPlayerStyle.innerProgressCompleted, { flex: flexCompleted }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { flex: flexRemaining }]} /> <View style={[mediaPlayerStyle.innerProgressRemaining, { flex: flexRemaining }]} />
</View> </View>
</View> </View>}
{this.state.areControlsVisible && {this.state.areControlsVisible &&
<View style={[mediaPlayerStyle.seekerHandle, { left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }> <View style={[mediaPlayerStyle.seekerHandle, { left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>

View file

@ -20,15 +20,18 @@ import Video from 'react-native-video';
import filePageStyle from '../../styles/filePage'; import filePageStyle from '../../styles/filePage';
class FilePage extends React.PureComponent { class FilePage extends React.PureComponent {
state = {
mediaLoaded: false,
fullscreenMode: false
};
static navigationOptions = { static navigationOptions = {
title: '' title: ''
}; };
constructor(props) {
super(props);
this.state = {
mediaLoaded: false,
fullscreenMode: false
};
}
componentDidMount() { componentDidMount() {
StatusBar.setHidden(false); StatusBar.setHidden(false);
this.fetchFileInfo(this.props); this.fetchFileInfo(this.props);
@ -128,27 +131,34 @@ class FilePage extends React.PureComponent {
const mediaType = Lbry.getMediaType(contentType); const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim; const { height, channel_name: channelName, value } = claim;
const showActions = !this.state.fullscreenMode && (completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes)); const showActions = !this.state.fullscreenMode &&
(completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
const channelClaimId = const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId; value && value.publisherSignature && value.publisherSignature.certificateId;
const playerStyle = [filePageStyle.player, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer];
const playerBgStyle = [filePageStyle.playerBackground, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayerBackground : filePageStyle.containedPlayerBackground];
// at least 2MB (or the full download) before media can be loaded // at least 2MB (or the full download) before media can be loaded
const canLoadMedia = fileInfo && (fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes); // 2MB = 1024*1024*2 const canLoadMedia = fileInfo &&
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes); // 2MB = 1024*1024*2
return ( return (
<View style={filePageStyle.pageContainer}> <View style={filePageStyle.pageContainer}>
<View style={this.state.fullscreenMode ? filePageStyle.fullscreenMedia : filePageStyle.mediaContainer}> <View style={filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !canLoadMedia)) && {(!fileInfo || (isPlayable && !canLoadMedia)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />} <FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />} {isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{!completed && !canLoadMedia && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />} {!completed && !canLoadMedia && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />}
{canLoadMedia && <View style={filePageStyle.playerBackground} />} </View>
{canLoadMedia && <MediaPlayer fileInfo={fileInfo} {canLoadMedia && <View style={playerBgStyle} />}
{canLoadMedia && <MediaPlayer fileInfo={fileInfo}
uri={navigation.state.params.uri} uri={navigation.state.params.uri}
style={filePageStyle.player} style={playerStyle}
onFullscreenToggled={this.handleFullscreenToggle} onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>} onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>}
</View>
{ showActions && { showActions &&
<View style={filePageStyle.actions}> <View style={filePageStyle.actions}>
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />} {completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}

View file

@ -17,15 +17,6 @@ const filePageStyle = StyleSheet.create({
width: screenWidth, width: screenWidth,
height: 220 height: 220
}, },
playerBackground: {
position: 'absolute',
flex: 1,
left: 0,
top: 0,
right: 0,
bottom: 16,
backgroundColor: Colors.Black
},
emptyClaimText: { emptyClaimText: {
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',
textAlign: 'center', textAlign: 'center',
@ -77,21 +68,37 @@ const filePageStyle = StyleSheet.create({
top: '40%' top: '40%'
}, },
player: { player: {
flex: 1,
width: '100%',
height: '100%',
marginBottom: 14,
zIndex: 99
},
fullscreenMedia: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
top: 0, top: 0,
zIndex: 101
},
containedPlayer: {
width: '100%',
height: 220,
},
fullscreenPlayer: {
width: '100%',
height: '100%',
right: 0, right: 0,
bottom: 0, bottom: 0
flex: 1, },
backgroundColor: Colors.Black, playerBackground: {
zIndex: 100 position: 'absolute',
left: 0,
top: 0,
zIndex: 100,
backgroundColor: Colors.Black
},
containedPlayerBackground: {
width: '100%',
height: 206,
},
fullscreenPlayerBackground: {
width: '100%',
height: '100%',
right: 0,
bottom: 0
}, },
actions: { actions: {
paddingLeft: 16, paddingLeft: 16,

View file

@ -1,21 +1,29 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Colors from './colors';
const mediaPlayerStyle = StyleSheet.create({ const mediaPlayerStyle = StyleSheet.create({
player: { player: {
flex: 1 flex: 1,
width: '100%',
height: '100%'
}, },
container: { container: {
marginBottom: 0, flex: 1,
paddingBottom: 16, paddingBottom: 16
},
fullscreenContainer: {
flex: 1,
justifyContent: 'center'
}, },
progress: { progress: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
overflow: 'hidden', width: '100%',
height: 3
}, },
innerProgressCompleted: { innerProgressCompleted: {
height: 4, height: 4,
backgroundColor: '#40c0a9', backgroundColor: Colors.LbryGreen,
}, },
innerProgressRemaining: { innerProgressRemaining: {
height: 4, height: 4,
@ -23,10 +31,16 @@ const mediaPlayerStyle = StyleSheet.create({
}, },
trackingControls: { trackingControls: {
height: 3, height: 3,
width: '100%',
position: 'absolute', position: 'absolute',
bottom: 14, bottom: 14
},
containedTrackingControls: {
left: 0, left: 0,
width: '100%'
},
fullscreenTrackingControls: {
alignSelf: 'center',
width: '70%'
}, },
playerControls: { playerControls: {
position: 'absolute', position: 'absolute',
@ -77,23 +91,24 @@ const mediaPlayerStyle = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
position: 'relative', position: 'relative',
top: 14, top: 14,
left: 8, left: 15,
height: 12, height: 12,
width: 12, width: 12,
backgroundColor: '#40c0a9' backgroundColor: '#40c0a9'
}, },
seekerHandle: { seekerHandle: {
backgroundColor: 'transparent',
position: 'absolute', position: 'absolute',
height: 36, height: 36,
width: 36, width: 48,
bottom: 0, bottom: 0,
marginLeft: -8 marginLeft: -16
}, },
bigSeekerCircle: { bigSeekerCircle: {
borderRadius: 24, borderRadius: 24,
position: 'relative', position: 'relative',
top: 8, top: 8,
left: 8, left: 15,
height: 24, height: 24,
width: 24, width: 24,
backgroundColor: '#40c0a9' backgroundColor: '#40c0a9'