From 22311a3edfd3d844aed9c76d45e84aff5eb1d740 Mon Sep 17 00:00:00 2001 From: Martin Santangelo <msantang78@gmail.com> Date: Fri, 1 Nov 2019 11:51:25 -0300 Subject: [PATCH] (fix) ios video preview and upload with ph:// paths --- package.json | 1 + src/capture/CapturePreview.js | 34 +++--- src/common/stores/AttachmentStore.js | 168 +++++++++++---------------- src/media/MindsVideo.js | 29 +++-- yarn.lock | 5 + 5 files changed, 112 insertions(+), 125 deletions(-) diff --git a/package.json b/package.json index 4acb4c0b97..05f4472f6b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-native-actionsheet": "^2.4.2", "react-native-animatable": "^1.3.3", "react-native-collapsible-header-views": "^1.0.2", + "react-native-convert-ph-asset": "^1.0.3", "react-native-device-info": "^4.0.1", "react-native-device-log": "Minds/react-native-device-log#74f06b09c6656aa228a9a3a474c714d82abf509e", "react-native-elements": "^0.19.1", diff --git a/src/capture/CapturePreview.js b/src/capture/CapturePreview.js index 45cc4d9b03..33d3713c80 100644 --- a/src/capture/CapturePreview.js +++ b/src/capture/CapturePreview.js @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { StyleSheet, View, Image } from 'react-native'; +import { StyleSheet, View, Image, Platform } from 'react-native'; import MindsVideo from '../media/MindsVideo'; @@ -15,25 +15,29 @@ export default class CapturePreview extends PureComponent { switch (this.props.type) { case 'image/gif': case 'image/jpeg': + case 'image': default: - body = <Image - resizeMode='contain' - source={{ uri: this.props.uri }} - style={styles.preview} - /> + body = ( + <Image + resizeMode="contain" + source={{uri: this.props.uri}} + style={styles.preview} + /> + ); break; case 'video/mp4': - body = <View style={styles.preview}> - <MindsVideo video={{ 'uri': this.props.uri }} /> - </View> + case 'video/quicktime': + case 'video/x-m4v': + case 'video': + body = ( + <View style={styles.preview}> + <MindsVideo video={{uri: this.props.uri}} /> + </View> + ); break; } - return ( - <View style={styles.wrapper}> - {body} - </View> - ); + return <View style={styles.wrapper}>{body}</View>; } } @@ -43,7 +47,7 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', alignItems: 'stretch', - backgroundColor: 'black' + backgroundColor: 'black', }, preview: { flex: 1, diff --git a/src/common/stores/AttachmentStore.js b/src/common/stores/AttachmentStore.js index e41f47605f..ff01601cb3 100644 --- a/src/common/stores/AttachmentStore.js +++ b/src/common/stores/AttachmentStore.js @@ -1,14 +1,11 @@ -import { observable, action, extendObservable } from 'mobx' -import { Platform, Alert } from 'react-native'; -import rnFS from 'react-native-fs'; -import MediaMeta from 'react-native-media-meta'; -import fileType from 'react-native-file-type'; +import {observable, action} from 'mobx'; +import {Alert, Platform} from 'react-native'; +import RNConvertPhAsset from 'react-native-convert-ph-asset'; import attachmentService from '../services/attachment.service'; -import {MINDS_MAX_VIDEO_LENGTH} from '../../config/Config'; -import mindsService from '../services/minds.service'; import logService from '../services/log.service'; import i18n from '../services/i18n.service'; +import mindsService from '../services/minds.service'; /** * Attachment Store @@ -16,15 +13,14 @@ import i18n from '../services/i18n.service'; export default class AttachmentStore { @observable hasAttachment = false; @observable uploading = false; - @observable checkingVideoLength = false; @observable progress = 0; - - guid = ''; - - @observable uri = ''; + @observable uri = ''; @observable type = ''; @observable license = ''; - tempIosVideo = ''; + + guid = ''; + fileName = null; + transcoding = false; /** * Attach media @@ -33,14 +29,10 @@ export default class AttachmentStore { */ @action async attachMedia(media, extra = null) { - - // no new media acepted if we are checking for video length - if (this.checkingVideoLength) return; - - // validate media - const valid = await this.validate(media); - if (!valid) return; - + if (this.transcoding) { + return; + } + console.log('ATTACHING', media, extra); if (this.uploading) { // abort current upload this.cancelCurrentUpload(); @@ -50,16 +42,45 @@ export default class AttachmentStore { await attachmentService.deleteMedia(this.guid); } catch (error) { // we ignore delete error for now - logService.info('Error deleting the uploaded media '+this.guid); + logService.info('Error deleting the uploaded media ' + this.guid); } } - this.uri = media.uri; - this.type = media.type; + if (!await this.validate(media)) { + return; + } + this.setHasAttachment(true); + // correctly handle videos from ph:// paths on ios + if ( + Platform.OS === 'ios' && + media.type === 'video' && + media.uri.startsWith('ph://') + ) { + try { + this.transcoding = true; + const converted = await RNConvertPhAsset.convertVideoFromUrl({ + url: media.uri, + convertTo: 'm4v', + quality: 'high', + }); + media.type = converted.mimeType; + media.uri = converted.path; + media.filename = converted.filename; + } catch (error) { + Alert.alert('Error reading the video', 'Please try again'); + } finally { + this.transcoding = false; + } + } + + this.uri = media.uri; + this.type = media.type; + this.fileName = media.fileName; + try { - const uploadPromise = attachmentService.attachMedia(media, extra, (pct) => { + const uploadPromise = attachmentService.attachMedia(media, extra, pct => { this.setProgress(pct); }); @@ -70,7 +91,9 @@ export default class AttachmentStore { const result = await uploadPromise; // ignore canceled - if ((uploadPromise.isCanceled && uploadPromise.isCanceled()) || !result) return; + if ((uploadPromise.isCanceled && uploadPromise.isCanceled()) || !result) { + return; + } this.guid = result.guid; } catch (err) { this.clear(); @@ -79,82 +102,31 @@ export default class AttachmentStore { this.setUploading(false); } - // delete temp ios video if necessary - if (this.tempIosVideo) { - rnFS.unlink(this.tempIosVideo); - this.tempIosVideo = ''; - } - return this.guid; } - /** - * Cancel current upload promise and request - */ - cancelCurrentUpload(clear=true) - { - this.uploadPromise && this.uploadPromise.cancel(() => { - if (clear) this.clear(); - }); - } - - /** - * Validate media - * @param {object} media - */ - @action async validate(media) { - - if(!media.type){ - const type = await fileType(media.path); - media.type = type.mime; - } - - if (media.fileName && media.fileName.includes(' ')) media.fileName = media.fileName.replace(/\s/g, "_"); - const settings = await mindsService.getSettings(); - - let videoPath = null; - switch (media.type) { - case 'video/mp4': - videoPath = media.path || media.uri.replace(/^.*:\/\//, ''); - break; - case 'ALAssetTypeVideo': - // if video is selected from cameraroll we need to copy - await this.copyVideoIos(media); - videoPath = this.tempIosVideo; - media.type = 'video/mp4'; - media.path = videoPath; - media.uri = 'file:\/\/'+videoPath; - break; + if (media.duration && media.duration > settings.max_video_length * 1000) { + Alert.alert( + i18n.t('sorry'), + i18n.t('attachment.tooLong', {minutes: settings.max_video_length / 60}), + ); + return false; } - - if (videoPath) { - this.checkingVideoLength = true; - const meta = await MediaMeta.get(videoPath); - - this.checkingVideoLength = false; - - // check video length - if (meta.duration && meta.duration > (settings.max_video_length * 1000) ) { - Alert.alert( - i18n.t('sorry'), - i18n.t('attachment.tooLong', {minutes: (settings.max_video_length / 60)}) - ); - return false; - } - } - return true; } /** - * copy a video from ios library assets to temporal app folder - * @param {object} media + * Cancel current upload promise and request */ - copyVideoIos(media) { - this.tempIosVideo = rnFS.TemporaryDirectoryPath+'MINDS-'+Date.now()+'.MP4' - return rnFS.copyAssetsVideoIOS(media.uri, this.tempIosVideo); + cancelCurrentUpload(clear = true) { + this.uploadPromise && + this.uploadPromise.cancel(() => { + if (clear) { + this.clear(); + } + }); } /** @@ -165,7 +137,7 @@ export default class AttachmentStore { try { attachmentService.deleteMedia(this.guid); this.clear(); - return true + return true; } catch (err) { return false; } @@ -177,12 +149,12 @@ export default class AttachmentStore { @action setProgress(value) { - this.progress = value + this.progress = value; } @action setUploading(value) { - this.uploading = value + this.uploading = value; } @action @@ -205,11 +177,5 @@ export default class AttachmentStore { this.checkingVideoLength = false; this.uploading = false; this.progress = 0; - - if (this.tempIosVideo) { - rnFS.unlink(this.tempIosVideo); - this.tempIosVideo = ''; - } } - -} \ No newline at end of file +} diff --git a/src/media/MindsVideo.js b/src/media/MindsVideo.js index 2850dbc2fc..926dcedc20 100644 --- a/src/media/MindsVideo.js +++ b/src/media/MindsVideo.js @@ -40,28 +40,39 @@ class MindsVideo extends Component { paused: true, volume: 1, loaded: true, - active: false, + active: !props.entity, showOverlay: true, fullScreen: false, error: false, inProgress: false, + video: {}, }; } + /** + * Derive state from props + * @param {object} nextProps + * @param {object} prevState + */ + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.video && nextProps.video.uri !== prevState.video.uri) { + return { + video: {uri: nextProps.video.uri}, + }; + } + return null; + } + /** * On component will mount */ - componentWillMount () { + componentDidMount() { this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => true, onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponderCapture: (evt, gestureState) => true - }) - - if (!this.props.entity) { - this.setState({active: true}) - } + }); this.onScreenBlur = this.props.navigation.addListener( 'didBlur', @@ -103,7 +114,7 @@ class MindsVideo extends Component { }; onError = (err) => { - logService.exception('[MindsVideo]', err) + logService.exception('[MindsVideo]', new Error(err)); this.setState({ error: true, inProgress: false, }); }; @@ -244,7 +255,7 @@ class MindsVideo extends Component { onProgress = {this.onProgress} onError={this.onError} ignoreSilentSwitch={'obey'} - source={{ uri: video.uri.replace('file://',''), type: 'mp4' }} + source={this.state.video} paused={paused} fullscreen={this.state.fullScreen} resizeMode={"contain"} diff --git a/yarn.lock b/yarn.lock index 4694bfa65c..e5c96c7c71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7376,6 +7376,11 @@ react-native-collapsible-header-views@^1.0.2: dependencies: fast-memoize "^2.5.1" +react-native-convert-ph-asset@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/react-native-convert-ph-asset/-/react-native-convert-ph-asset-1.0.3.tgz#af299a6a3f7270b55c3bdfe472a40cea02c93c80" + integrity sha512-qdTrUsDlxOvC7KnamjjRnxaijr3myfOwzbYR0vyTl0BjPT25jkCQ9a4Q26egRdy8uqiw7mqB1ZhG5OohwnpsOg== + react-native-crypto@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/react-native-crypto/-/react-native-crypto-2.2.0.tgz#c999ed7c96064f830e1f958687f53d0c44025770" -- GitLab