From e0401de1a335e667f21b62bcfeb253d94aedabd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Wed, 9 Mar 2016 16:54:11 +0100 Subject: [PATCH 1/3] Add async tiff thumbnail encoding support - Fixes #171 --- .../ascribe_detail/media_container.js | 77 +++++++++++++------ js/components/ascribe_media/media_player.js | 65 +++++++++++----- js/constants/application_constants.js | 4 +- sass/ascribe_media_player.scss | 10 +++ 4 files changed, 113 insertions(+), 43 deletions(-) diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 5511fdb4..45ea81a2 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -15,6 +15,8 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; import AclProxy from '../acl_proxy'; +import AppConstants from '../../constants/application_constants'; + import { getLangText } from '../../utils/lang_utils'; import { extractFileExtensionFromString } from '../../utils/file_utils'; @@ -23,7 +25,7 @@ const EMBED_IFRAME_HEIGHT = { video: 315, audio: 62 }; -const ENCODE_UPDATE_TIME = 5000; + let MediaContainer = React.createClass({ propTypes: { @@ -41,21 +43,18 @@ let MediaContainer = React.createClass({ componentDidMount() { const { content: { digital_work: digitalWork }, refreshObject } = this.props; + const { timerId } = this.state; - if (digitalWork) { - const isEncoding = digitalWork.isEncoding; - - if (digitalWork.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { - this.setState({ - timerId: window.setInterval(refreshObject, ENCODE_UPDATE_TIME) - }); - } + if (digitalWork && this.isDigitalWorkEncoding() && !timerId) { + this.setState({ timerId: window.setInterval(refreshObject, AppConstants.encodeUpdateThreshold) }); } }, componentWillUpdate() { - if (this.props.content.digital_work.isEncoding === 100) { - window.clearInterval(this.state.timerId); + const { timerId } = this.state; + + if (!this.isDigitalWorkEncoding() && timerId) { + window.clearInterval(timerId); } }, @@ -63,13 +62,35 @@ let MediaContainer = React.createClass({ window.clearInterval(this.state.timerId); }, - render() { - const { content, currentUser } = this.props; - // Pieces and editions are joined to the user by a foreign key in the database, so - // the information in content will be updated if a user updates their username. - // We also force uniqueness of usernames, so this check is safe to dtermine if the - // content was registered by the current user. - const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered); + /* + * A digital work is encoding if: + * + * - it's either a video that has `isEncoding` < 100 + * - of a .tif|.tiff file that has not yet been converted + * to a .jpeg thumbnail + */ + isDigitalWorkEncoding() { + const { + content: { + digital_work: digitalWork, + thumbnail + } } = this.props; + + return ( + ( + digitalWork.mime === 'video' && + typeof digitalWork.isEncoding === 'number' && + digitalWork.isEncoding !== 100 + ) + || + ( + digitalWork.mime === 'image' && + (this.getFileExtensionFromUrl(thumbnail.url) === 'tif' || this.getFileExtensionFromUrl(thumbnail.url) === 'tiff') + ) + ); + }, + + getFileExtensionFromUrl(url) { // We want to show the file's extension as a label of the download button. // We can however not only use `extractFileExtensionFromString` on the url for that @@ -78,8 +99,18 @@ let MediaContainer = React.createClass({ // domain: e.g. '.net/live/'. // Therefore, we extract the file's name (last part of url, separated with a slash) // and try to extract the file extension from there. - const fileName = content.digital_work.url.split('/').pop(); - const fileExtension = extractFileExtensionFromString(fileName); + const fileName = url.split('/').pop(); + return extractFileExtensionFromString(fileName); + }, + + render() { + const { content, currentUser } = this.props; + + // Pieces and editions are joined to the user by a foreign key in the database, so + // the information in content will be updated if a user updates their username. + // We also force uniqueness of usernames, so this check is safe to dtermine if the + // content was registered by the current user. + const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered); let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ? content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe; @@ -117,7 +148,8 @@ let MediaContainer = React.createClass({
@@ -127,7 +159,6 @@ let MediaContainer = React.createClass({ - + fileExtension={this.getFileExtensionFromUrl(content.digital_work.url)} /> {embed}

diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index 1552b44c..7811be78 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -6,9 +6,12 @@ import Q from 'q'; import Panel from 'react-bootstrap/lib/Panel'; import ProgressBar from 'react-bootstrap/lib/ProgressBar'; +import AscribeSpinner from '../ascribe_spinner'; + import AppConstants from '../../constants/application_constants'; import { escapeHTML } from '../../utils/general_utils'; +import { getLangText } from '../../utils/lang_utils'; import { InjectInHeadUtils } from '../../utils/inject_utils'; /** @@ -31,18 +34,18 @@ let Other = React.createClass({ render() { let filename = this.props.url.split('/').pop(); let tokens = filename.split('.'); - let preview; + let thumbnail; if (tokens.length > 1) { - preview = '.' + tokens.pop(); + thumbnail = '.' + tokens.pop(); } else { - preview = 'file'; + thumbnail = 'file'; } return (

- {preview} + {thumbnail}

); @@ -52,11 +55,11 @@ let Other = React.createClass({ let Image = React.createClass({ propTypes: { url: React.PropTypes.string, - preview: React.PropTypes.string.isRequired + thumbnail: React.PropTypes.string.isRequired }, componentDidMount() { - if(this.props.url) { + if (this.props.url) { InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl) .then(() => Q.all([ @@ -67,15 +70,15 @@ let Image = React.createClass({ }, render() { - const { url, preview } = this.props; + const { url, thumbnail } = this.props; - if(url) { + if (url) { return ( - + ); } else { return ( - + ); } } @@ -130,7 +133,7 @@ let Video = React.createClass({ */ propTypes: { - preview: React.PropTypes.string.isRequired, + thumbnail: React.PropTypes.string.isRequired, url: React.PropTypes.string.isRequired, extraData: React.PropTypes.array.isRequired, encodingStatus: React.PropTypes.number @@ -170,7 +173,7 @@ let Video = React.createClass({ prepareVideoHTML() { let sources = this.props.extraData.map((data) => ''); let html = [ - '']; @@ -184,7 +187,7 @@ let Video = React.createClass({ ); } else { return ( - + ); } } @@ -200,20 +203,31 @@ let resourceMap = { let MediaPlayer = React.createClass({ propTypes: { mimetype: React.PropTypes.oneOf(['image', 'video', 'audio', 'pdf', 'other']).isRequired, - preview: React.PropTypes.string.isRequired, + thumbnail: React.PropTypes.string.isRequired, + thumbnailFileExtension: React.PropTypes.string, url: React.PropTypes.string.isRequired, extraData: React.PropTypes.array, - encodingStatus: React.PropTypes.number + encodingStatus: React.PropTypes.number, + }, + + isVideoEncoding() { + const { mimetype, encodingStatus } = this.props; + return mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100; + }, + + isImageEncoding() { + const { mimetype, thumbnailFileExtension } = this.props; + return mimetype === 'image' && (thumbnailFileExtension === 'tif' || thumbnailFileExtension === 'tiff'); }, render() { const { mimetype, - preview, + thumbnail, url, extraData, encodingStatus } = this.props; - if (mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100) { + if (this.isVideoEncoding()) { return (

@@ -225,10 +239,23 @@ let MediaPlayer = React.createClass({ className="ascribe-progress-bar" />

); + } else if (this.isImageEncoding()) { + return ( +
+ + + {getLangText('We successfully received your image and it is now being encoded.')} +
+ {getLangText('We will be refreshing this page as soon as encoding has finished.')} +
+ {getLangText('(You may close this page)')} +
+
+ ); } else { let Component = resourceMap[mimetype] || Other; let componentProps = { - preview, + thumbnail, url, extraData, encodingStatus @@ -242,7 +269,7 @@ let MediaPlayer = React.createClass({ // // If this is the case, we disable shmui by deleting the original `url` prop and replace // the assigned component to `Image`. - if(!decodeURIComponent(preview).match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/) && + if (!decodeURIComponent(thumbnail).match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/) && Component === Other) { Component = resourceMap.image; delete componentProps.url; diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 897926a3..042e336e 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -92,13 +92,15 @@ const constants = { 'searchThreshold': 500, + 'encodeUpdateThreshold': 5000, + 'supportedThumbnailFileFormats': [ 'x-sgi-movie', 'x-msvideo', 'quicktime', 'mpeg', 'png', 'jpeg', 'gif', 'ogg', 'oga', 'ogv', 'ogx', 'wmv', 'wma', 'flv', '3gpp2', '3p2', '3pg', 'png', 'jpg', 'jpeg', 'gif', '264', '3g', '3g2', '3gp', '3gp2', '3gpp', 'mp4', 'm4a', 'm4v', 'f4v', 'f4a', 'm4b', 'm4r', 'f4b', 'mov', 'quicktime', 'webm', 'x264', 'mpeg', 'mpeg4', 'mpg4', 'bmp', 'eps', 'jp2', 'j2k', 'jpm', - 'mj2' + 'mj2', 'tif', 'tiff' ], // in case of whitelabel customization, we store stuff here diff --git a/sass/ascribe_media_player.scss b/sass/ascribe_media_player.scss index 25d4aa64..681adae1 100644 --- a/sass/ascribe_media_player.scss +++ b/sass/ascribe_media_player.scss @@ -115,5 +115,15 @@ .vjs-default-skin .vjs-control-bar { background-color: rgba(0,0,0,.7); } + + &.encoding-image { + text-align: center; + margin: 2em 0 2em 0; + + em { + display: block; + margin-top: 1em; + } + } } From 4fdab505e7b9bc184fb4f3b41e85f10dc5574a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Thu, 10 Mar 2016 14:33:54 +0100 Subject: [PATCH 2/3] Restructure MediaPlayer - encoding evaluation taken out to MediaContainer --- .../ascribe_detail/media_container.js | 86 +++++++++++-------- js/components/ascribe_media/media_player.js | 60 +++---------- 2 files changed, 61 insertions(+), 85 deletions(-) diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 45ea81a2..9d340ab8 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -3,6 +3,7 @@ import React from 'react'; import Button from 'react-bootstrap/lib/Button'; +import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import MediaPlayer from './../ascribe_media/media_player'; @@ -13,6 +14,8 @@ import S3DownloadButton from '../ascribe_buttons/s3_download_button'; import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; +import AscribeSpinner from '../ascribe_spinner'; + import AclProxy from '../acl_proxy'; import AppConstants from '../../constants/application_constants'; @@ -45,7 +48,7 @@ let MediaContainer = React.createClass({ const { content: { digital_work: digitalWork }, refreshObject } = this.props; const { timerId } = this.state; - if (digitalWork && this.isDigitalWorkEncoding() && !timerId) { + if (digitalWork && (this.isVideoEncoding() || this.isImageEncoding()) && !timerId) { this.setState({ timerId: window.setInterval(refreshObject, AppConstants.encodeUpdateThreshold) }); } }, @@ -53,7 +56,7 @@ let MediaContainer = React.createClass({ componentWillUpdate() { const { timerId } = this.state; - if (!this.isDigitalWorkEncoding() && timerId) { + if (!(this.isVideoEncoding() || this.isImageEncoding()) && timerId) { window.clearInterval(timerId); } }, @@ -62,36 +65,50 @@ let MediaContainer = React.createClass({ window.clearInterval(this.state.timerId); }, - /* - * A digital work is encoding if: - * - * - it's either a video that has `isEncoding` < 100 - * - of a .tif|.tiff file that has not yet been converted - * to a .jpeg thumbnail - */ - isDigitalWorkEncoding() { - const { - content: { - digital_work: digitalWork, - thumbnail - } } = this.props; + isVideoEncoding() { + const { content: { digital_work: digitalWork } } = this.props; + return digitalWork.mime === 'video' && digitalWork.isEncoding === 'number' && digitalWork.isEncoding !== 100; + }, - return ( - ( - digitalWork.mime === 'video' && - typeof digitalWork.isEncoding === 'number' && - digitalWork.isEncoding !== 100 - ) - || - ( - digitalWork.mime === 'image' && - (this.getFileExtensionFromUrl(thumbnail.url) === 'tif' || this.getFileExtensionFromUrl(thumbnail.url) === 'tiff') - ) - ); + isImageEncoding() { + const { content: { thumbnail, digital_work: digitalWork } } = this.props; + const thumbnailFileExtension = this.getFileExtensionFromUrl(thumbnail.thumbnail_sizes['600x600']); + + return digitalWork.mime === 'image' && (thumbnailFileExtension === 'tif' || thumbnailFileExtension === 'tiff'); + }, + + getEncodingMessage() { + if (this.isVideoEncoding()) { + const { digital_work: digitalWork } = this.props; + + return ( +
+

+ We successfully received your video and it is now being encoded. +
You can leave this page and check back on the status later.
+

+ +
+ ); + } else if (this.isImageEncoding()) { + return ( +
+ + + {getLangText('We successfully received your image and it is now being encoded.')} +
+ {getLangText('We will be refreshing this page as soon as encoding has finished.')} +
+ {getLangText('(You may close this page)')} +
+
+ ); + } }, getFileExtensionFromUrl(url) { - // We want to show the file's extension as a label of the download button. // We can however not only use `extractFileExtensionFromString` on the url for that // as files might be saved on S3 without a file extension which leads @@ -105,19 +122,17 @@ let MediaContainer = React.createClass({ render() { const { content, currentUser } = this.props; - + const mimetype = content.digital_work.mime; // Pieces and editions are joined to the user by a foreign key in the database, so // the information in content will be updated if a user updates their username. // We also force uniqueness of usernames, so this check is safe to dtermine if the // content was registered by the current user. const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered); + const thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ? content.thumbnail.thumbnail_sizes['600x600'] + : content.thumbnail.url_safe; - let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ? - content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe; - let mimetype = content.digital_work.mime; let embed = null; let extraData = null; - let isEmbedDisabled = mimetype === 'video' && content.digital_work.isEncoding !== undefined && content.digital_work.isEncoding !== 100; if (content.digital_work.encoding_urls) { extraData = content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; }); @@ -132,7 +147,7 @@ let MediaContainer = React.createClass({ } @@ -149,10 +164,9 @@ let MediaContainer = React.createClass({ + encodingMessage={this.getEncodingMessage()} />

diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index 7811be78..93fe3995 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -4,14 +4,10 @@ import React from 'react'; import Q from 'q'; import Panel from 'react-bootstrap/lib/Panel'; -import ProgressBar from 'react-bootstrap/lib/ProgressBar'; - -import AscribeSpinner from '../ascribe_spinner'; import AppConstants from '../../constants/application_constants'; import { escapeHTML } from '../../utils/general_utils'; -import { getLangText } from '../../utils/lang_utils'; import { InjectInHeadUtils } from '../../utils/inject_utils'; /** @@ -135,8 +131,7 @@ let Video = React.createClass({ propTypes: { thumbnail: React.PropTypes.string.isRequired, url: React.PropTypes.string.isRequired, - extraData: React.PropTypes.array.isRequired, - encodingStatus: React.PropTypes.number + extraData: React.PropTypes.array.isRequired }, getInitialState() { @@ -204,61 +199,28 @@ let MediaPlayer = React.createClass({ propTypes: { mimetype: React.PropTypes.oneOf(['image', 'video', 'audio', 'pdf', 'other']).isRequired, thumbnail: React.PropTypes.string.isRequired, - thumbnailFileExtension: React.PropTypes.string, url: React.PropTypes.string.isRequired, extraData: React.PropTypes.array, - encodingStatus: React.PropTypes.number, - }, - - isVideoEncoding() { - const { mimetype, encodingStatus } = this.props; - return mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100; - }, - - isImageEncoding() { - const { mimetype, thumbnailFileExtension } = this.props; - return mimetype === 'image' && (thumbnailFileExtension === 'tif' || thumbnailFileExtension === 'tiff'); + encodingMessage: React.PropTypes.node }, render() { - const { mimetype, - thumbnail, - url, - extraData, - encodingStatus } = this.props; + const { + mimetype, + thumbnail, + url, + extraData, + encodingMessage + } = this.props; - if (this.isVideoEncoding()) { - return ( -

-

- We successfully received your video and it is now being encoded. -
You can leave this page and check back on the status later.
-

- -
- ); - } else if (this.isImageEncoding()) { - return ( -
- - - {getLangText('We successfully received your image and it is now being encoded.')} -
- {getLangText('We will be refreshing this page as soon as encoding has finished.')} -
- {getLangText('(You may close this page)')} -
-
- ); + if (encodingMessage) { + return encodingMessage; } else { let Component = resourceMap[mimetype] || Other; let componentProps = { thumbnail, url, extraData, - encodingStatus }; // Since the launch of the portfolio whitelabel submission, From 7a5996025594c0d2dd861fe4cd726ce10e8a5195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Thu, 10 Mar 2016 15:09:22 +0100 Subject: [PATCH 3/3] Restrict thumbnail expansion if file is TIFF --- .../ascribe_detail/media_container.js | 19 ++++--------------- js/components/ascribe_media/media_player.js | 5 ++++- js/utils/file_utils.js | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 9d340ab8..c414dee2 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -21,7 +21,8 @@ import AclProxy from '../acl_proxy'; import AppConstants from '../../constants/application_constants'; import { getLangText } from '../../utils/lang_utils'; -import { extractFileExtensionFromString } from '../../utils/file_utils'; + +import { extractFileExtensionFromUrl } from '../../utils/file_utils'; const EMBED_IFRAME_HEIGHT = { @@ -72,7 +73,7 @@ let MediaContainer = React.createClass({ isImageEncoding() { const { content: { thumbnail, digital_work: digitalWork } } = this.props; - const thumbnailFileExtension = this.getFileExtensionFromUrl(thumbnail.thumbnail_sizes['600x600']); + const thumbnailFileExtension = extractFileExtensionFromUrl(thumbnail.thumbnail_sizes['600x600']); return digitalWork.mime === 'image' && (thumbnailFileExtension === 'tif' || thumbnailFileExtension === 'tiff'); }, @@ -108,18 +109,6 @@ let MediaContainer = React.createClass({ } }, - getFileExtensionFromUrl(url) { - // We want to show the file's extension as a label of the download button. - // We can however not only use `extractFileExtensionFromString` on the url for that - // as files might be saved on S3 without a file extension which leads - // `extractFileExtensionFromString` to extract everything starting from the top level - // domain: e.g. '.net/live/'. - // Therefore, we extract the file's name (last part of url, separated with a slash) - // and try to extract the file extension from there. - const fileName = url.split('/').pop(); - return extractFileExtensionFromString(fileName); - }, - render() { const { content, currentUser } = this.props; const mimetype = content.digital_work.mime; @@ -181,7 +170,7 @@ let MediaContainer = React.createClass({ url={content.digital_work.url} title={content.title} artistName={content.artist_name} - fileExtension={this.getFileExtensionFromUrl(content.digital_work.url)} /> + fileExtension={extractFileExtensionFromUrl(content.digital_work.url)} /> {embed}

diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index 93fe3995..4cfb4bc2 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -8,6 +8,7 @@ import Panel from 'react-bootstrap/lib/Panel'; import AppConstants from '../../constants/application_constants'; import { escapeHTML } from '../../utils/general_utils'; +import { extractFileExtensionFromUrl } from '../../utils/file_utils'; import { InjectInHeadUtils } from '../../utils/inject_utils'; /** @@ -67,8 +68,10 @@ let Image = React.createClass({ render() { const { url, thumbnail } = this.props; + const urlFileExtension = extractFileExtensionFromUrl(url); - if (url) { + // TIFFs can not be displayed by the browser, so we just display their thumbnail + if (url && urlFileExtension !== 'tif' && urlFileExtension !== 'tiff') { return ( ); diff --git a/js/utils/file_utils.js b/js/utils/file_utils.js index 1e4998f8..7557536d 100644 --- a/js/utils/file_utils.js +++ b/js/utils/file_utils.js @@ -102,3 +102,17 @@ export function extractFileExtensionFromString(s) { return explodedFileName.length > 1 ? explodedFileName.pop() : ''; } + + +/** + * Extracts a file extension from a url. + * + * If a file without an extension is submitted (file), then + * this method just returns an empty string. + * @param {string} url A url ending in a file + * @return {string} a file extension + */ +export function extractFileExtensionFromUrl(url) { + const fileName = url.split('/').pop(); + return extractFileExtensionFromString(fileName); +}