Add async tiff thumbnail encoding support

- Fixes #171
This commit is contained in:
Tim Daubenschütz 2016-03-09 16:54:11 +01:00
parent 5e6b483149
commit e0401de1a3
4 changed files with 113 additions and 43 deletions

View File

@ -15,6 +15,8 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
import AclProxy from '../acl_proxy'; import AclProxy from '../acl_proxy';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { extractFileExtensionFromString } from '../../utils/file_utils'; import { extractFileExtensionFromString } from '../../utils/file_utils';
@ -23,7 +25,7 @@ const EMBED_IFRAME_HEIGHT = {
video: 315, video: 315,
audio: 62 audio: 62
}; };
const ENCODE_UPDATE_TIME = 5000;
let MediaContainer = React.createClass({ let MediaContainer = React.createClass({
propTypes: { propTypes: {
@ -41,21 +43,18 @@ let MediaContainer = React.createClass({
componentDidMount() { componentDidMount() {
const { content: { digital_work: digitalWork }, refreshObject } = this.props; const { content: { digital_work: digitalWork }, refreshObject } = this.props;
const { timerId } = this.state;
if (digitalWork) { if (digitalWork && this.isDigitalWorkEncoding() && !timerId) {
const isEncoding = digitalWork.isEncoding; this.setState({ timerId: window.setInterval(refreshObject, AppConstants.encodeUpdateThreshold) });
if (digitalWork.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
this.setState({
timerId: window.setInterval(refreshObject, ENCODE_UPDATE_TIME)
});
}
} }
}, },
componentWillUpdate() { componentWillUpdate() {
if (this.props.content.digital_work.isEncoding === 100) { const { timerId } = this.state;
window.clearInterval(this.state.timerId);
if (!this.isDigitalWorkEncoding() && timerId) {
window.clearInterval(timerId);
} }
}, },
@ -63,13 +62,35 @@ let MediaContainer = React.createClass({
window.clearInterval(this.state.timerId); window.clearInterval(this.state.timerId);
}, },
render() { /*
const { content, currentUser } = this.props; * A digital work is encoding if:
// 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. * - it's either a video that has `isEncoding` < 100
// We also force uniqueness of usernames, so this check is safe to dtermine if the * - of a .tif|.tiff file that has not yet been converted
// content was registered by the current user. * to a .jpeg thumbnail
const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered); */
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 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 // 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/<hash>'. // domain: e.g. '.net/live/<hash>'.
// Therefore, we extract the file's name (last part of url, separated with a slash) // Therefore, we extract the file's name (last part of url, separated with a slash)
// and try to extract the file extension from there. // and try to extract the file extension from there.
const fileName = content.digital_work.url.split('/').pop(); const fileName = url.split('/').pop();
const fileExtension = extractFileExtensionFromString(fileName); 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'] ? let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ?
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe; content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
@ -117,7 +148,8 @@ let MediaContainer = React.createClass({
<div> <div>
<MediaPlayer <MediaPlayer
mimetype={mimetype} mimetype={mimetype}
preview={thumbnail} thumbnail={thumbnail}
thumbnailFileExtension={this.getFileExtensionFromUrl(thumbnail)}
url={content.digital_work.url} url={content.digital_work.url}
extraData={extraData} extraData={extraData}
encodingStatus={content.digital_work.isEncoding} /> encodingStatus={content.digital_work.isEncoding} />
@ -127,7 +159,6 @@ let MediaContainer = React.createClass({
<TwitterShareButton <TwitterShareButton
text={getLangText('Check out %s ascribed piece', didUserRegisterContent ? 'my latest' : 'this' )} /> text={getLangText('Check out %s ascribed piece', didUserRegisterContent ? 'my latest' : 'this' )} />
</span> </span>
<AclProxy <AclProxy
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download} show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download}
aclObject={content.acl} aclObject={content.acl}
@ -136,7 +167,7 @@ let MediaContainer = React.createClass({
url={content.digital_work.url} url={content.digital_work.url}
title={content.title} title={content.title}
artistName={content.artist_name} artistName={content.artist_name}
fileExtension={fileExtension} /> fileExtension={this.getFileExtensionFromUrl(content.digital_work.url)} />
</AclProxy> </AclProxy>
{embed} {embed}
</p> </p>

View File

@ -6,9 +6,12 @@ import Q from 'q';
import Panel from 'react-bootstrap/lib/Panel'; import Panel from 'react-bootstrap/lib/Panel';
import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AscribeSpinner from '../ascribe_spinner';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { escapeHTML } from '../../utils/general_utils'; import { escapeHTML } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { InjectInHeadUtils } from '../../utils/inject_utils'; import { InjectInHeadUtils } from '../../utils/inject_utils';
/** /**
@ -31,18 +34,18 @@ let Other = React.createClass({
render() { render() {
let filename = this.props.url.split('/').pop(); let filename = this.props.url.split('/').pop();
let tokens = filename.split('.'); let tokens = filename.split('.');
let preview; let thumbnail;
if (tokens.length > 1) { if (tokens.length > 1) {
preview = '.' + tokens.pop(); thumbnail = '.' + tokens.pop();
} else { } else {
preview = 'file'; thumbnail = 'file';
} }
return ( return (
<Panel className="media-other"> <Panel className="media-other">
<p className="text-center"> <p className="text-center">
{preview} {thumbnail}
</p> </p>
</Panel> </Panel>
); );
@ -52,11 +55,11 @@ let Other = React.createClass({
let Image = React.createClass({ let Image = React.createClass({
propTypes: { propTypes: {
url: React.PropTypes.string, url: React.PropTypes.string,
preview: React.PropTypes.string.isRequired thumbnail: React.PropTypes.string.isRequired
}, },
componentDidMount() { componentDidMount() {
if(this.props.url) { if (this.props.url) {
InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl) InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl)
.then(() => .then(() =>
Q.all([ Q.all([
@ -67,15 +70,15 @@ let Image = React.createClass({
}, },
render() { render() {
const { url, preview } = this.props; const { url, thumbnail } = this.props;
if(url) { if (url) {
return ( return (
<img className="shmui-ascribe" src={preview} data-large-src={url}/> <img className="shmui-ascribe" src={thumbnail} data-large-src={url} />
); );
} else { } else {
return ( return (
<img src={preview}/> <img src={thumbnail} />
); );
} }
} }
@ -130,7 +133,7 @@ let Video = React.createClass({
*/ */
propTypes: { propTypes: {
preview: React.PropTypes.string.isRequired, thumbnail: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired, url: React.PropTypes.string.isRequired,
extraData: React.PropTypes.array.isRequired, extraData: React.PropTypes.array.isRequired,
encodingStatus: React.PropTypes.number encodingStatus: React.PropTypes.number
@ -170,7 +173,7 @@ let Video = React.createClass({
prepareVideoHTML() { prepareVideoHTML() {
let sources = this.props.extraData.map((data) => '<source type="video/' + data.type + '" src="' + escapeHTML(data.url) + '" />'); let sources = this.props.extraData.map((data) => '<source type="video/' + data.type + '" src="' + escapeHTML(data.url) + '" />');
let html = [ let html = [
'<video id="mainvideo" class="video-js vjs-default-skin" poster="' + escapeHTML(this.props.preview) + '"', '<video id="mainvideo" class="video-js vjs-default-skin" poster="' + escapeHTML(this.props.thumbnail) + '"',
'controls preload="none" width="auto" height="auto">', 'controls preload="none" width="auto" height="auto">',
sources.join('\n'), sources.join('\n'),
'</video>']; '</video>'];
@ -184,7 +187,7 @@ let Video = React.createClass({
); );
} else { } else {
return ( return (
<Image preview={this.props.preview} /> <Image thumbnail={this.props.thumbnail} />
); );
} }
} }
@ -200,20 +203,31 @@ let resourceMap = {
let MediaPlayer = React.createClass({ let MediaPlayer = React.createClass({
propTypes: { propTypes: {
mimetype: React.PropTypes.oneOf(['image', 'video', 'audio', 'pdf', 'other']).isRequired, 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, url: React.PropTypes.string.isRequired,
extraData: React.PropTypes.array, 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() { render() {
const { mimetype, const { mimetype,
preview, thumbnail,
url, url,
extraData, extraData,
encodingStatus } = this.props; encodingStatus } = this.props;
if (mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100) { if (this.isVideoEncoding()) {
return ( return (
<div className="ascribe-detail-header ascribe-media-player"> <div className="ascribe-detail-header ascribe-media-player">
<p> <p>
@ -225,10 +239,23 @@ let MediaPlayer = React.createClass({
className="ascribe-progress-bar" /> className="ascribe-progress-bar" />
</div> </div>
); );
} else if (this.isImageEncoding()) {
return (
<div className="ascribe-media-player encoding-image">
<AscribeSpinner color='dark-blue' size='lg' />
<em className="text-center">
{getLangText('We successfully received your image and it is now being encoded.')}
<br />
{getLangText('We will be refreshing this page as soon as encoding has finished.')}
<br />
{getLangText('(You may close this page)')}
</em>
</div>
);
} else { } else {
let Component = resourceMap[mimetype] || Other; let Component = resourceMap[mimetype] || Other;
let componentProps = { let componentProps = {
preview, thumbnail,
url, url,
extraData, extraData,
encodingStatus 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 // If this is the case, we disable shmui by deleting the original `url` prop and replace
// the assigned component to `Image`. // 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 === Other) {
Component = resourceMap.image; Component = resourceMap.image;
delete componentProps.url; delete componentProps.url;

View File

@ -92,13 +92,15 @@ const constants = {
'searchThreshold': 500, 'searchThreshold': 500,
'encodeUpdateThreshold': 5000,
'supportedThumbnailFileFormats': [ 'supportedThumbnailFileFormats': [
'x-sgi-movie', 'x-msvideo', 'quicktime', 'mpeg', 'png', 'jpeg', 'gif', 'x-sgi-movie', 'x-msvideo', 'quicktime', 'mpeg', 'png', 'jpeg', 'gif',
'ogg', 'oga', 'ogv', 'ogx', 'wmv', 'wma', 'flv', '3gpp2', '3p2', '3pg', 'ogg', 'oga', 'ogv', 'ogx', 'wmv', 'wma', 'flv', '3gpp2', '3p2', '3pg',
'png', 'jpg', 'jpeg', 'gif', '264', '3g', '3g2', '3gp', '3gp2', '3gpp', 'png', 'jpg', 'jpeg', 'gif', '264', '3g', '3g2', '3gp', '3gp2', '3gpp',
'mp4', 'm4a', 'm4v', 'f4v', 'f4a', 'm4b', 'm4r', 'f4b', 'mov', 'quicktime', 'mp4', 'm4a', 'm4v', 'f4v', 'f4a', 'm4b', 'm4r', 'f4b', 'mov', 'quicktime',
'webm', 'x264', 'mpeg', 'mpeg4', 'mpg4', 'bmp', 'eps', 'jp2', 'j2k', 'jpm', 'webm', 'x264', 'mpeg', 'mpeg4', 'mpg4', 'bmp', 'eps', 'jp2', 'j2k', 'jpm',
'mj2' 'mj2', 'tif', 'tiff'
], ],
// in case of whitelabel customization, we store stuff here // in case of whitelabel customization, we store stuff here

View File

@ -115,5 +115,15 @@
.vjs-default-skin .vjs-control-bar { .vjs-default-skin .vjs-control-bar {
background-color: rgba(0,0,0,.7); background-color: rgba(0,0,0,.7);
} }
&.encoding-image {
text-align: center;
margin: 2em 0 2em 0;
em {
display: block;
margin-top: 1em;
}
}
} }