Skip to content
Commits on Source (2)
......@@ -97,11 +97,9 @@ export default class MediaView extends Component {
guid = this.props.entity.guid;
}
const source = {uri: MINDS_API_URI + `api/v1/media/${guid}/play`, headers: api.buildHeaders() };
return (
<View style={styles.videoContainer}>
<MindsVideo video={source} entity={this.props.entity} ref={o => {this.videoPlayer = o}}/>
<MindsVideo entity={this.props.entity} ref={o => {this.videoPlayer = o}}/>
</View>
);
}
......
......@@ -6,16 +6,12 @@ import Cancelable from 'promise-cancelable';
* Attacment service
*/
class AttachmentService {
/**
* Attach media file
* @param {object} media
* @param {function} onProgress
*/
attachMedia(media, extra, onProgress=null) {
let type = 'image'
attachMedia(media, extra, onProgress = null) {
const file = {
uri: media.uri,
path: media.path || null,
......@@ -25,8 +21,10 @@ class AttachmentService {
const progress = (e) => {
let pct = e.loaded / e.total;
if (onProgress) onProgress(pct);
}
if (onProgress) {
onProgress(pct);
}
};
let promise;
......@@ -87,6 +85,10 @@ class AttachmentService {
return api.get(`api/v1/media/transcoding/${guid}`);
}
getVideoSources(guid) {
return api.get(`api/v2/media/video/${guid}`);
}
/**
* Capture video
*/
......@@ -98,8 +100,8 @@ class AttachmentService {
uri: response.uri,
path: response.path,
type: 'video/mp4',
fileName: 'image.mp4'
}
fileName: 'image.mp4',
};
}
return response;
......
import React, {
Component,
PropTypes
} from 'react';
import React, {Component} from 'react';
// workaround to fix tooltips on android
import Tooltip from "rne-modal-tooltip";
import {
PanResponder,
......@@ -20,22 +20,29 @@ import ProgressBar from './ProgressBar';
let FORWARD_DURATION = 7;
import { observer, inject } from 'mobx-react/native';
import {observer} from 'mobx-react/native';
import Icon from 'react-native-vector-icons/Ionicons';
import { withNavigation } from 'react-navigation';
import { CommonStyle as CS } from '../styles/Common';
import {withNavigation} from 'react-navigation';
import {CommonStyle as CS} from '../styles/Common';
import colors from '../styles/Colors';
import ExplicitImage from '../common/components/explicit/ExplicitImage';
import logService from '../common/services/log.service';
import i18n from '../common/services/i18n.service';
import attachmentService from '../common/services/attachment.service';
import videoPlayerService from '../common/services/video-player.service';
import apiService from '../common/services/api.service';
const isIOS = Platform.OS === 'ios';
@observer
class MindsVideo extends Component {
/**
* Constructor
*
* @param {*} props
* @param {*} context
* @param {...any} args
*/
constructor(props, context, ...args) {
super(props, context, ...args);
this.state = {
......@@ -49,6 +56,8 @@ class MindsVideo extends Component {
inProgress: false,
video: {},
transcoding: false,
sources: null,
source: 0,
};
}
......@@ -74,15 +83,12 @@ class MindsVideo extends Component {
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
});
this.onScreenBlur = this.props.navigation.addListener(
'didBlur',
() => {
this.pause();
}
);
this.onScreenBlur = this.props.navigation.addListener('didBlur', () => {
this.pause();
});
}
/**
......@@ -95,13 +101,19 @@ class MindsVideo extends Component {
}
}
/**
* On video end
*/
onVideoEnd = () => {
this.setState({key: new Date(), currentTime: 0, paused: true}, () => {
this.player.seek(0);
});
}
};
onVideoLoad = (e) => {
/**
* On video load
*/
onVideoLoad = e => {
let current = 0;
if (this.state.changedModeTime > 0) {
current = this.state.changedModeTime;
......@@ -113,16 +125,24 @@ class MindsVideo extends Component {
this.player.seek(current);
this.onLoadEnd();
}
};
/**
* On load start
*/
onLoadStart = () => {
this.setState({error: false, inProgress: true});
};
/**
* On error
*/
onError = async err => {
const entity = this.props.entity;
try {
const response = await attachmentService.isTranscoding(entity.entity_guid);
const response = await attachmentService.isTranscoding(
entity.entity_guid,
);
if (response.transcoding) {
this.setState({transcoding: true});
} else {
......@@ -135,25 +155,43 @@ class MindsVideo extends Component {
}
};
/**
* On load end
*/
onLoadEnd = () => {
this.setState({error: false, inProgress: false});
};
/**
* Toggle sound
*/
toggleVolume = () => {
const v = this.state.volume ? 0 : 1;
this.setState({volume: v});
};
/**
* On progress
*/
onProgress = e => {
this.setState({currentTime: e.currentTime});
};
/**
* On backward
* @param {number} currentTime
*/
onBackward(currentTime) {
let newTime = Math.max(currentTime - FORWARD_DURATION, 0);
this.player.seek(newTime);
this.setState({currentTime: newTime});
}
/**
* On forward
* @param {number} currentTime
* @param {number} duration
*/
onForward(currentTime, duration) {
if (currentTime + FORWARD_DURATION > duration) {
this.onVideoEnd();
......@@ -164,6 +202,11 @@ class MindsVideo extends Component {
}
}
/**
* Get current time percentage
* @param {number} currentTime
* @param {number} duration
*/
getCurrentTimePercentage(currentTime, duration) {
if (currentTime > 0) {
return parseFloat(currentTime) / parseFloat(duration);
......@@ -172,53 +215,91 @@ class MindsVideo extends Component {
}
}
/**
* On progress changed
* @param {number} newPercent
* @param {boolean} paused
*/
onProgressChanged(newPercent, paused) {
let {duration} = this.state;
let newTime = newPercent * duration / 100;
let newTime = (newPercent * duration) / 100;
this.setState({currentTime: newTime, paused: paused});
this.player.seek(newTime);
}
/**
* Toggle full-screen
*/
toggleFullscreen = () => {
this.setState({fullScreen: !this.state.fullScreen});
}
};
play = () => {
/**
* Play the current video and activate the player
*/
play = async () => {
videoPlayerService.setCurrent(this);
this.setState({
const state = {
active: true,
showOverlay: false,
paused: false,
});
};
if (!this.state.sources && this.props.entity) {
const response = await attachmentService.getVideoSources(this.props.entity.entity_guid);
state.sources = response.sources.filter(v => v.type === 'video/mp4');
state.video = {
uri: state.sources[0].src,
headers: apiService.buildHeaders(),
};
}
this.setState(state);
};
/**
* Pause the video
*/
pause = () => {
this.setState({
paused: true,
});
}
};
/**
* Play button
*/
get play_button() {
const size = 56;
if (this.state.paused) {
return <Icon
onPress={this.play}
return (
<Icon
onPress={this.play}
style={styles.videoIcon}
name="md-play"
size={size}
color={colors.light}
/>
);
}
return (
<Icon
onPress={this.pause}
style={styles.videoIcon}
name="md-play"
name="md-pause"
size={size}
color={colors.light}
/>;
}
return <Icon
onPress={this.pause}
style={styles.videoIcon}
name="md-pause"
size={size}
color={colors.light}
/>;
/>
);
}
/**
* Show control overlay (hide debounced 4 seconds)
*/
openControlOverlay = () => {
if (!this.state.showOverlay) {
this.setState({
......@@ -227,63 +308,160 @@ class MindsVideo extends Component {
}
this.hideOverlay();
}
};
/**
* Hide overlay
*/
hideOverlay = _.debounce(() => {
if (this.state.showOverlay) {
this.setState({
showOverlay: false,
});
}
}, 4000)
}, 4000);
/**
* Full screen icon
*/
get fullScreen() {
return <Icon onPress={this.toggleFullscreen} name="md-resize" size={23} color={colors.light} style={CS.paddingLeft}/>;
return (
<Icon
onPress={this.toggleFullscreen}
name="ios-expand"
size={23}
color={colors.light}
style={CS.paddingLeft}
/>
);
}
/**
* Settings icon
*/
get settingsIcon() {
if (!this.props.entity) {
return null;
}
return (
<View style={styles.controlSettingsContainer}>
<Tooltip
popover={this.sourceSelector}
withOverlay={false}
height={60}
onOpen={this.openControlOverlay}
backgroundColor="rgba(48,48,48,0.7)">
<Icon
name="ios-settings"
size={23}
color={colors.light}
style={CS.paddingLeft}
/>
</Tooltip>
</View>
);
}
/**
* Source selector
*/
get sourceSelector() {
if (!this.state.sources) {
return null;
}
return (
<View>
{this.state.sources.map((s, i) => (
<Text
style={[
CS.colorWhite,
CS.fontL,
CS.paddingBottom,
i === this.state.source ? CS.bold : null,
]}
onPress={() => this.setState({source: i})}>
{s.size}p
</Text>
))}
</View>
);
}
changeSource(index) {
this.setState({
source: index,
video: {
uri: this.state.sources[index].src,
headers: apiService.buildHeaders(),
},
});
}
get volumeIcon() {
if (this.state.volume == 0) {
return <Icon onPress={this.toggleVolume} name="ios-volume-off" size={23} color={colors.light} />;
if (this.state.volume === 0) {
return (
<Icon
onPress={this.toggleVolume}
name="ios-volume-off"
size={23}
color={colors.light}
/>
);
} else {
return <Icon onPress={this.toggleVolume} name="ios-volume-high" size={23} color={colors.light} />;
return (
<Icon
onPress={this.toggleVolume}
name="ios-volume-high"
size={23}
color={colors.light}
/>
);
}
}
/**
* Set the reference to the video player
*/
setRef = (ref) => {
setRef = ref => {
this.player = ref;
};
onFullscreenPlayerDidDismiss = () => {
this.setState({fullScreen: false, paused: true});
};
/**
* Get video component or thumb
*/
get video() {
let { video, entity } = this.props;
let { paused, volume } = this.state;
const thumb_uri = entity ? (entity.get('custom_data.thumbnail_src') || entity.thumbnail_src) : null;
let {video, entity} = this.props;
let {paused, volume} = this.state;
const thumb_uri = entity
? entity.get('custom_data.thumbnail_src') || entity.thumbnail_src
: null;
if (this.state.active || !thumb_uri) {
return (
<Video
key={`video${this.state.source}`}
ref={this.setRef}
volume={parseFloat(this.state.volume)}
onEnd={this.onVideoEnd}
onLoadStart={this.onLoadStart}
onLoad={this.onVideoLoad}
onProgress = {this.onProgress}
onProgress={this.onProgress}
onError={this.onError}
ignoreSilentSwitch={'obey'}
source={this.state.video}
paused={paused}
fullscreen={this.state.fullScreen}
resizeMode={"contain"}
controls={isIOS}
onFullscreenPlayerDidDismiss={this.onFullscreenPlayerDidDismiss}
resizeMode={'contain'}
controls={false}
style={CS.flexContainer}
/>
)
);
} else {
const image = { uri: thumb_uri };
const image = {uri: thumb_uri};
return (
<ExplicitImage
onLoadEnd={this.onLoadEnd}
......@@ -293,7 +471,7 @@ class MindsVideo extends Component {
style={[CS.positionAbsolute]}
// loadingIndicator="placeholder"
/>
)
);
}
}
......@@ -301,71 +479,98 @@ class MindsVideo extends Component {
* Render overlay
*/
renderOverlay() {
// no overlay on full screen
if (this.state.fullScreen) return null;
if (this.state.fullScreen) {
return null;
}
const entity = this.props.entity;
let {currentTime, duration, paused} = this.state;
const mustShow = (this.state.showOverlay && !isIOS) || this.state.paused && entity;
const mustShow =
(this.state.showOverlay) || (this.state.paused && entity);
if (mustShow) {
const completedPercentage = this.getCurrentTimePercentage(currentTime, duration) * 100;
const completedPercentage =
this.getCurrentTimePercentage(currentTime, duration) * 100;
const progressBar = (
<View style={styles.progressBarContainer}>
<ProgressBar duration={duration}
<ProgressBar
duration={duration}
currentTime={currentTime}
percent={completedPercentage}
onNewPercent={this.onProgressChanged.bind(this)}
/>
/>
</View>
);
return (
<TouchableWithoutFeedback
style={styles.controlOverlayContainer}
onPress={this.openControlOverlay}
>
style={styles.controlOverlayContainer}
onPress={this.openControlOverlay}>
<View style={styles.controlOverlayContainer}>
<View style={[CS.positionAbsolute, CS.centered, CS.marginTop2x]}>
{this.play_button}
</View>
{ (this.player && !isIOS) && <View style={styles.controlBarContainer}>
{ progressBar }
<View style={[CS.padding, CS.rowJustifySpaceEvenly]}>
{this.volumeIcon}
{this.player && this.settingsIcon}
{this.player && (
<View style={styles.controlBarContainer}>
{isIOS && <View style={[CS.padding, CS.rowJustifySpaceEvenly, CS.marginRight]}>
{this.fullScreen}
</View>}
{progressBar}
<View style={[CS.padding, CS.rowJustifySpaceEvenly]}>
{this.volumeIcon}
</View>
</View>
</View> }
)}
</View>
</TouchableWithoutFeedback>
)
);
}
return null;
}
/**
* Render error overlay
*/
renderErrorOverlay() {
return (
<View style={styles.controlOverlayContainer}>
<Text
style={styles.errorText}
>{i18n.t('errorMediaDisplay')}</Text>
<Text style={styles.errorText}>{i18n.t('errorMediaDisplay')}</Text>
</View>
);
}
/**
* Render in progress overlay
*/
renderInProgressOverlay() {
return (<View style={[styles.controlOverlayContainer, styles.controlOverlayContainerTransparent]}>
<ActivityIndicator size="large" />
</View>);
return (
<View
style={[
styles.controlOverlayContainer,
styles.controlOverlayContainerTransparent,
]}>
<ActivityIndicator size="large" />
</View>
);
}
/**
* Render transcoding overlay
*/
renderTranscodingOverlay() {
return (
<View
style={[styles.controlOverlayContainer, styles.controlOverlayContainerTransparent]}>
<Text style={styles.errorText}>{i18n.t('transcodingMediaDisplay')}</Text>
style={[
styles.controlOverlayContainer,
styles.controlOverlayContainerTransparent,
]}>
<Text style={styles.errorText}>
{i18n.t('transcodingMediaDisplay')}
</Text>
</View>
);
}
......@@ -374,22 +579,22 @@ class MindsVideo extends Component {
* Render
*/
render() {
const { error, inProgress, transcoding } = this.state;
const {error, inProgress, transcoding} = this.state;
const overlay = this.renderOverlay();
return (
<View style={[CS.flexContainer, CS.backgroundBlack]} >
<View style={[CS.flexContainer, CS.backgroundBlack]}>
<TouchableWithoutFeedback
style={CS.flexContainer}
onPress={this.openControlOverlay}>
{ this.video }
{this.video}
</TouchableWithoutFeedback>
{ inProgress && this.renderInProgressOverlay() }
{ !inProgress && error && this.renderErrorOverlay() }
{ transcoding && this.renderTranscodingOverlay() }
{ !inProgress && !error && !transcoding && overlay }
{inProgress && this.renderInProgressOverlay()}
{!inProgress && error && this.renderErrorOverlay()}
{transcoding && this.renderTranscodingOverlay()}
{!inProgress && !error && !transcoding && overlay}
</View>
)
);
}
}
......@@ -398,8 +603,8 @@ let styles = StyleSheet.create({
position: 'absolute',
top: 0,
left: 0,
right:0,
bottom:0,
right: 0,
bottom: 0,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
......@@ -416,37 +621,46 @@ let styles = StyleSheet.create({
errorText: {
color: colors.darkGreyed,
},
controlSettingsContainer: {
position: 'absolute',
top: 0,
right: 0,
margin: 8,
paddingRight: 5,
borderRadius: 3,
backgroundColor: 'rgba(48,48,48,0.7)',
},
controlBarContainer: {
flexDirection: 'row',
position: 'absolute',
bottom:0,
left:0,
right:0,
bottom: 0,
left: 0,
right: 0,
alignItems: 'stretch',
margin: 8,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 3,
backgroundColor: 'rgba(48,48,48,0.7)'
backgroundColor: 'rgba(48,48,48,0.7)',
},
progressBarContainer: {
flex: 1,
flexDirection: 'column',
alignItems: 'center',
},
fullScreen: {backgroundColor: "black"},
fullScreen: {backgroundColor: 'black'},
progressBar: {
alignSelf: "stretch",
margin: 20
alignSelf: 'stretch',
margin: 20,
},
videoIcon: {
position: "relative",
alignSelf: "center",
position: 'relative',
alignSelf: 'center',
bottom: 0,
left: 0,
right: 0,
top: 0
}
top: 0,
},
});
export default withNavigation(MindsVideo);
......@@ -8261,7 +8261,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
......@@ -8595,6 +8595,19 @@ react-native-device-log@Minds/react-native-device-log#74f06b09c6656aa228a9a3a474
react-native-invertible-scroll-view "^1.1.1"
stacktrace-parser "^0.1.4"
react-native-elements@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-native-elements/-/react-native-elements-1.1.0.tgz#f99bcda4459a886f3ab4591c684c099d37aedf2b"
integrity sha512-n1eOL0kUdlH01zX7bn1p7qhYXn7kquqxYQ0oWlxoAck9t5Db/KeK5ViOsAk8seYSvAG6Pe7OxgzRFnMfFhng0Q==
dependencies:
color "^3.1.0"
deepmerge "^3.1.0"
hoist-non-react-statics "^3.1.0"
opencollective-postinstall "^2.0.0"
prop-types "^15.5.8"
react-native-ratings "^6.3.0"
react-native-status-bar-height "^2.2.0"
react-native-elements@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/react-native-elements/-/react-native-elements-1.2.7.tgz#1eca2db715c41722aeb67aea62bd2a4621adb134"
......@@ -9339,6 +9352,12 @@ rn-apk@^0.2.9:
resolved "https://registry.yarnpkg.com/rn-apk/-/rn-apk-0.2.9.tgz#6aec783dd64cdf6074b0a590cc51261999351b79"
integrity sha512-JcqO9raQWdjQAaRKjz6OFdE6G4GzZnqhqMLRyKHQ9WBaYFzhNpzujTyTr0T9cngvMJ1JOmHfxG1U8DFHf7QU3A==
"rne-modal-tooltip@gist:b28c003d87c619674def0878473338a0":
version "1.1.0"
resolved "https://gist.github.com/b28c003d87c619674def0878473338a0.git#b68c9d067545ce72b0aa4c9bf5a5fea90d136bc0"
dependencies:
react-native-elements "1.1.0"
rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
......