Merge pull request #33 from lbryio/media-player

implemented rudimentary media seeker. Still needs some tweaking.
This commit is contained in:
akinwale 2018-03-22 07:50:05 +01:00 committed by GitHub
commit c6e0c6b39a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2578 additions and 626 deletions

View file

@ -20,7 +20,8 @@ const discoverStack = StackNavigator({
File: {
screen: FilePage,
navigationOptions: {
header: null
header: null,
drawerLockMode: 'locked-closed'
}
}
}, {

View file

@ -58,7 +58,7 @@ class FileItem extends React.PureComponent {
this.props.navigation.navigate('File', { uri: uri });
}
}>
<FileItemMedia title={title} thumbnail={thumbnail} />
<FileItemMedia title={title} thumbnail={thumbnail} resizeMode="cover" />
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />
<Text style={discoverStyle.fileItemName}>{title}</Text>
{channelName &&

View file

@ -28,7 +28,7 @@ class FileItemMedia extends React.PureComponent {
render() {
let style = this.props.style;
const { title, thumbnail } = this.props;
const { title, thumbnail, resizeMode } = this.props;
const atStyle = this.state.autoThumbStyle;
if (thumbnail && ((typeof thumbnail) === 'string')) {
@ -37,7 +37,7 @@ class FileItemMedia extends React.PureComponent {
}
return (
<Image source={{uri: thumbnail}} resizeMode="cover" style={style} />
<Image source={{uri: thumbnail}} resizeMode={resizeMode ? resizeMode : "cover"} style={style} />
);
}

View file

@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import MediaPlayer from './view';
const select = state => ({});
const perform = dispatch => ({});
export default connect(select, perform)(MediaPlayer);

View file

@ -0,0 +1,273 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import { PanResponder, Text, View, ScrollView, TouchableOpacity } from 'react-native';
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome';
import FileItemMedia from '../fileItemMedia';
import mediaPlayerStyle from '../../styles/mediaPlayer';
class MediaPlayer extends React.PureComponent {
static ControlsTimeout = 3000;
seekResponder = null;
seekerWidth = 0;
video = null;
state = {
rate: 1,
volume: 1,
muted: false,
resizeMode: 'stretch',
duration: 0.0,
currentTime: 0.0,
paused: false,
fullscreenMode: false,
areControlsVisible: true,
controlsTimeout: -1,
seekerOffset: 0,
seekerPosition: 0,
firstPlay: true
};
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
});
if (this.props.onMediaLoaded) {
this.props.onMediaLoaded();
}
}
onProgress = (data) => {
this.setState({ currentTime: data.currentTime });
if (!this.state.seeking) {
this.setSeekerPosition(this.calculateSeekerPosition());
}
if (this.state.firstPlay) {
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();
}
hidePlayerControls() {
const player = this;
let timeout = setTimeout(() => {
player.setState({ areControlsVisible: false });
}, MediaPlayer.ControlsTimeout);
player.setState({ controlsTimeout: timeout });
}
togglePlay = () => {
this.showPlayerControls();
this.setState({ paused: !this.state.paused });
}
toggleFullscreenMode = () => {
this.showPlayerControls();
const { onFullscreenToggled } = this.props;
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
this.setState({ resizeMode: this.state.fullscreenMode ? 'contain' : 'stretch' });
if (onFullscreenToggled) {
onFullscreenToggled(this.state.fullscreenMode);
}
});
}
onEnd = () => {
this.setState({ paused: true });
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();
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.onEnd();
} else {
this.seekTo(time);
this.setState({ seeking: false });
}
this.hidePlayerControls();
}
});
}
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();
}
componentWillUnmount() {
this.setState({ paused: true, fullscreenMode: false });
const { onFullscreenToggled } = this.props;
if (onFullscreenToggled) {
onFullscreenToggled(false);
}
}
renderPlayerControls() {
if (this.state.areControlsVisible) {
return (
<View style={mediaPlayerStyle.playerControlsContainer}>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={32} 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;
}
render() {
const { fileInfo, title, thumbnail, style, fullScreenStyle } = this.props;
const flexCompleted = this.getCurrentTimePercentage() * 100;
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
return (
<View style={[style, mediaPlayerStyle.container]}>
<Video source={{ uri: 'file:///' + fileInfo.download_path }}
ref={(ref: Video) => { this.video = ref }}
resizeMode={this.state.resizeMode}
playInBackground={true}
style={mediaPlayerStyle.player}
rate={this.state.rate}
volume={this.state.volume}
paused={this.state.paused}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
/>
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.showPlayerControls}>
{this.renderPlayerControls()}
</TouchableOpacity>
<View style={mediaPlayerStyle.trackingControls}>
<View style={mediaPlayerStyle.progress} onLayout={(evt) => this.seekerWidth = evt.nativeEvent.layout.width}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { flex: flexCompleted }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { flex: flexRemaining }]} />
</View>
</View>
{this.state.areControlsVisible &&
<View style={[mediaPlayerStyle.seekerHandle, { left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
</View>}
</View>
);
}
}
export default MediaPlayer;

View file

@ -1,20 +1,16 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import { Text, View, ScrollView, TouchableOpacity } from 'react-native';
import Video from 'react-native-video';
import filePageStyle from '../../styles/filePage';
import { Text, View, ScrollView, StatusBar, TouchableOpacity, NativeModules } from 'react-native';
import FileItemMedia from '../../component/fileItemMedia';
import FileDownloadButton from '../../component/fileDownloadButton';
import MediaPlayer from '../../component/mediaPlayer';
import Video from 'react-native-video';
import filePageStyle from '../../styles/filePage';
class FilePage extends React.PureComponent {
state = {
rate: 1,
volume: 1,
muted: false,
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: true,
mediaLoaded: false,
fullscreenMode: false
};
static navigationOptions = {
@ -22,6 +18,7 @@ class FilePage extends React.PureComponent {
};
componentDidMount() {
StatusBar.setHidden(false);
this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props);
}
@ -42,6 +39,26 @@ class FilePage extends React.PureComponent {
}
}
handleFullscreenToggle = (mode) => {
this.setState({ fullscreenMode: mode });
StatusBar.setHidden(mode);
if (NativeModules.ScreenOrientation) {
if (mode) {
// fullscreen, so change orientation to landscape mode
NativeModules.ScreenOrientation.lockOrientationLandscape();
} else {
NativeModules.ScreenOrientation.unlockOrientation();
}
}
}
componentWillUnmount() {
StatusBar.setHidden(false);
if (NativeModules.ScreenOrientation) {
NativeModules.ScreenOrientation.unlockOrientation();
}
}
render() {
const {
claim,
@ -73,25 +90,14 @@ class FilePage extends React.PureComponent {
return (
<View style={filePageStyle.pageContainer}>
<View style={filePageStyle.mediaContainer}>
{(!fileInfo || !isPlayable) && <FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
<View style={this.state.fullscreenMode ? filePageStyle.fullscreenMedia : filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !this.state.mediaLoaded)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{!completed && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />}
{fileInfo && isPlayable &&
<TouchableOpacity
style={filePageStyle.player}
onPress={() => this.setState({ paused: !this.state.paused })}>
<Video source={{ uri: 'file:///' + fileInfo.download_path }}
resizeMode="cover"
playInBackground={true}
style={filePageStyle.player}
rate={this.state.rate}
volume={this.state.volume}
paused={this.state.paused}
/>
</TouchableOpacity>
}
{fileInfo && isPlayable && <MediaPlayer fileInfo={fileInfo}
style={filePageStyle.player}
onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>}
</View>
<ScrollView style={filePageStyle.scrollContainer}>
<Text style={filePageStyle.title}>{title}</Text>

View file

View file

View file

@ -9,11 +9,13 @@ const discoverStyle = StyleSheet.create({
flex: 1
},
title: {
fontFamily: 'Metropolis-Regular',
fontSize: 20,
textAlign: 'center',
margin: 10,
},
categoryName: {
fontFamily: 'Metropolis-Regular',
fontSize: 20,
marginLeft: 24,
marginTop: 16,
@ -26,15 +28,15 @@ const discoverStyle = StyleSheet.create({
marginBottom: 48
},
fileItemName: {
fontFamily: 'Metropolis-Bold',
marginTop: 8,
fontSize: 16,
fontWeight: 'bold'
fontSize: 16
},
channelName: {
fontFamily: 'Metropolis-SemiBold',
fontSize: 14,
marginTop: 4,
color: '#c0c0c0',
fontWeight: 'bold'
color: '#c0c0c0'
},
filePriceContainer: {
backgroundColor: '#61fcd8',
@ -47,10 +49,10 @@ const discoverStyle = StyleSheet.create({
borderRadius: 4
},
filePriceText: {
fontFamily: 'Metropolis-Bold',
fontSize: 12,
textAlign: 'center',
color: '#0c604b',
fontWeight: 'bold'
color: '#0c604b'
},
drawerHamburger: {
marginLeft: 8

View file

@ -1,18 +1,19 @@
import { StyleSheet } from 'react-native';
const fileDownloadButtonStyle = StyleSheet.create({
container: {
width: 120,
height: 36,
borderRadius: 18,
justifyContent: 'center',
backgroundColor: '#40c0a9',
},
text: {
color: '#ffffff',
fontSize: 13,
textAlign: 'center'
}
container: {
width: 120,
height: 36,
borderRadius: 18,
justifyContent: 'center',
backgroundColor: '#40c0a9',
},
text: {
fontFamily: 'Metropolis-Medium',
color: '#ffffff',
fontSize: 14,
textAlign: 'center'
}
});
export default fileDownloadButtonStyle;

View file

@ -5,11 +5,13 @@ const width = screenDimension.width - 48; // screen width minus combined left an
const fileItemMediaStyle = StyleSheet.create({
autothumb: {
width: width,
height: 180,
flex: 1,
width: '100%',
height: 200,
justifyContent: 'center'
},
autothumbText: {
fontFamily: 'Metropolis-SemiBold',
textAlign: 'center',
color: '#ffffff',
fontSize: 40
@ -48,8 +50,9 @@ const fileItemMediaStyle = StyleSheet.create({
backgroundColor: '#ffa726'
},
thumbnail: {
width: width,
height: 180,
flex: 1,
width: '100%',
height: 200,
shadowColor: 'transparent'
}
});

View file

@ -12,10 +12,12 @@ const filePageStyle = StyleSheet.create({
flex: 1
},
mediaContainer: {
backgroundColor: '#000000',
alignItems: 'center'
alignItems: 'center',
width: screenWidth,
height: 220,
},
emptyClaimText: {
fontFamily: 'Metropolis-Regular',
textAlign: 'center',
fontSize: 20,
marginLeft: 16,
@ -25,22 +27,23 @@ const filePageStyle = StyleSheet.create({
flex: 1
},
title: {
fontFamily: 'Metropolis-Bold',
fontSize: 24,
fontWeight: 'bold',
marginTop: 20,
marginTop: 12,
marginLeft: 20,
marginRight: 20,
marginBottom: 12
},
channelName: {
fontFamily: 'Metropolis-SemiBold',
fontSize: 20,
fontWeight: 'bold',
marginLeft: 20,
marginRight: 20,
marginBottom: 20,
color: '#9b9b9b'
},
description: {
fontFamily: 'Metropolis-Regular',
fontSize: 16,
marginLeft: 20,
marginRight: 20,
@ -56,8 +59,20 @@ const filePageStyle = StyleSheet.create({
top: '50%'
},
player: {
width: screenWidth,
height: 200
flex: 1,
width: '100%',
height: '100%',
marginBottom: 14
},
fullscreenMedia: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
flex: 1,
backgroundColor: '#000000',
zIndex: 100
}
});

View file

@ -0,0 +1,104 @@
import { StyleSheet, Dimensions } from 'react-native';
const screenDimension = Dimensions.get('window');
const screenWidth = screenDimension.width;
const mediaPlayerStyle = StyleSheet.create({
player: {
flex: 1
},
container: {
},
progress: {
flex: 1,
flexDirection: 'row',
overflow: 'hidden',
},
innerProgressCompleted: {
height: 4,
backgroundColor: '#40c0a9',
},
innerProgressRemaining: {
height: 4,
backgroundColor: '#2c2c2c',
},
trackingControls: {
height: 3,
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
},
playerControls: {
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
},
playerControlsContainer: {
backgroundColor: 'transparent',
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
playPauseButton: {
position: 'absolute',
width: 64,
height: 64,
alignItems: 'center',
justifyContent: 'center'
},
toggleFullscreenButton: {
position: 'absolute',
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'center',
right: 0,
bottom: 14,
},
elapsedDuration: {
fontFamily: 'Metropolis-Regular',
position: 'absolute',
left: 8,
bottom: 24,
fontSize: 12,
color: '#ffffff'
},
totalDuration: {
fontFamily: 'Metropolis-Regular',
position: 'absolute',
right: 40,
bottom: 24,
fontSize: 12,
color: '#ffffff'
},
seekerCircle: {
borderRadius: 12,
position: 'relative',
top: 8,
left: 8,
height: 12,
width: 12,
backgroundColor: '#40c0a9'
},
seekerHandle: {
position: 'absolute',
height: 28,
width: 28,
bottom: -12,
marginLeft: -8
},
bigSeekerCircle: {
borderRadius: 24,
position: 'relative',
top: 2,
left: 8,
height: 24,
width: 24,
backgroundColor: '#40c0a9'
}
});
export default mediaPlayerStyle;

View file

@ -7,13 +7,14 @@ const splashStyle = StyleSheet.create({
backgroundColor: '#40b89a'
},
title: {
fontFamily: 'Metropolis-Bold',
fontSize: 64,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 48,
color: '#ffffff'
},
details: {
fontFamily: 'Metropolis-Regular',
fontSize: 14,
marginLeft: 16,
marginRight: 16,
@ -21,7 +22,7 @@ const splashStyle = StyleSheet.create({
textAlign: 'center'
},
message: {
fontWeight: 'bold',
fontFamily: 'Metropolis-Bold',
fontSize: 18,
color: '#ffffff',
marginLeft: 16,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
·ѓ*Цї<D0A6>1rЏ 8Љ¤)Чт»EгЉ
WhèÑ'ï¢Ã,š|!ÚÊ.ÔÿòR

View file

@ -18,7 +18,7 @@ import java.util.Random;
* Created by akinwale on 3/15/18.
*/
public class LbryDownloadManagerModule extends ReactContextBaseJavaModule {
public class DownloadManagerModule extends ReactContextBaseJavaModule {
private Context context;
private HashMap<Integer, NotificationCompat.Builder> builders = new HashMap<Integer, NotificationCompat.Builder>();
@ -29,7 +29,7 @@ public class LbryDownloadManagerModule extends ReactContextBaseJavaModule {
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
public LbryDownloadManagerModule(ReactApplicationContext reactContext) {
public DownloadManagerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
}

View file

@ -0,0 +1,43 @@
package io.lbry.lbrynet.reactmodules;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
/**
* Created by akinwale on 3/19/18.
*/
public class ScreenOrientationModule extends ReactContextBaseJavaModule {
private Context context;
public ScreenOrientationModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
}
@Override
public String getName() {
return "ScreenOrientation";
}
@ReactMethod
public void unlockOrientation() {
Activity activity = getCurrentActivity();
if (activity != null) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
}
}
@ReactMethod
public void lockOrientationLandscape() {
Activity activity = getCurrentActivity();
if (activity != null) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
}
}

View file

@ -5,7 +5,8 @@ import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import io.lbry.lbrynet.reactmodules.LbryDownloadManagerModule;
import io.lbry.lbrynet.reactmodules.DownloadManagerModule;
import io.lbry.lbrynet.reactmodules.ScreenOrientationModule;
import java.util.ArrayList;
import java.util.Collections;
@ -21,8 +22,9 @@ public class LbryReactPackage implements ReactPackage {
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new LbryDownloadManagerModule(reactContext));
modules.add(new DownloadManagerModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext));
return modules;
}
}