Skip to content

Commit deaddd1

Browse files
author
Nir Maoz
authored
Fix video poster default frame to be the middle frame
1 parent 5c76ece commit deaddd1

File tree

2 files changed

+232
-122
lines changed

2 files changed

+232
-122
lines changed

src/components/Video/Video.js

Lines changed: 93 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,119 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import {Cloudinary, Transformation, Util} from 'cloudinary-core';
3+
import {Cloudinary, Util} from 'cloudinary-core';
44
import CloudinaryComponent from '../CloudinaryComponent';
55

6-
const DEFAULT_POSTER_OPTIONS = {
7-
format: 'jpg',
8-
resource_type: 'video'
9-
};
10-
116
/**
127
* A component representing a Cloudinary served video
138
*/
149
class Video extends CloudinaryComponent {
15-
constructor(props, context) {
16-
super(props, context);
17-
this.state = {};
18-
}
10+
/**
11+
* Merge context with props
12+
* @return {*}
13+
*/
14+
getMergedProps = () => {
15+
return {...this.getContext(), ...this.props};
16+
};
17+
18+
/**
19+
* Generate a video source url
20+
* @param cld - preconfigured cloudinary-core object
21+
* @param publicId - identifier of the video asset
22+
* @param childTransformations - child transformations for this video element
23+
* @param sourceTransformations - source transformations fot this video element
24+
* @param sourceType - format of the video url
25+
* @return {*}
26+
*/
27+
generateVideoUrl = (cld, publicId, childTransformations, sourceTransformations, sourceType) => {
28+
const sourceTransformation = sourceTransformations[sourceType] || {};
29+
const urlOptions = Util.defaults({}, sourceTransformation, childTransformations, {
30+
resource_type: 'video',
31+
format: sourceType
32+
});
33+
34+
return cld.url(publicId, urlOptions);
35+
};
36+
37+
/**
38+
* Generate <source> tags for this video element
39+
* @param cld - preconfigured cloudinary-core object
40+
* @param publicId - identifier of the video asset
41+
* @param childTransformations - child transformations fot this video element
42+
* @param sourceTransformations - source transformations for this video element
43+
* @param sourceTypes - formats for each video url that will be generated
44+
* @return {*}
45+
*/
46+
generateSources = (cld, publicId, childTransformations, sourceTransformations, sourceTypes) => (
47+
sourceTypes.map(sourceType => {
48+
const src = this.generateVideoUrl(cld, publicId, childTransformations, sourceTransformations, sourceType);
49+
const mimeType = 'video/' + (sourceType === 'ogv' ? 'ogg' : sourceType);
50+
51+
return <source key={mimeType} src={src} type={mimeType}/>;
52+
})
53+
);
54+
55+
/**
56+
* Get props for the video element that will be rendered
57+
* @return {{tagAttributes: Object, sources: [<source>] | string}}
58+
*/
59+
getVideoTagProps = () => {
60+
let {
61+
innerRef,
62+
publicId,
63+
fallback,
64+
children,
65+
sourceTypes = Cloudinary.DEFAULT_VIDEO_PARAMS.source_types,
66+
sourceTransformation = {},
67+
...options
68+
} = this.getMergedProps();
1969

20-
render() {
21-
let {publicId, poster, sourceTypes, fallback, sourceTransformation: sourceTransformations, innerRef, ...options} = Object.assign({},
22-
this.getContext(),
23-
this.props);
24-
sourceTransformations = sourceTransformations || {};
25-
sourceTypes = sourceTypes || Cloudinary.DEFAULT_VIDEO_PARAMS.source_types;
2670
options = CloudinaryComponent.normalizeOptions(options, {});
27-
let cld = Cloudinary.new(Util.withSnakeCaseKeys(options));
28-
let sources = [];
29-
let tagAttributes = Transformation.new(options).toHtmlAttributes();
30-
let childTransformations = this.getTransformation(options);
31-
if (Util.isPlainObject(poster)) {
32-
let defaults = poster.publicId !== undefined && poster.publicId !== null ? Cloudinary.DEFAULT_IMAGE_PARAMS : DEFAULT_POSTER_OPTIONS;
33-
poster = cld.url(poster.publicId || publicId, Util.defaults({}, Util.withSnakeCaseKeys(poster), defaults));
34-
}
35-
if (!Util.isEmpty(poster)) {
36-
tagAttributes.poster = poster;
37-
}
38-
if (!Util.isEmpty(this.state.poster)) {
39-
tagAttributes.poster = this.state.poster;
40-
}
71+
const snakeCaseOptions = Util.withSnakeCaseKeys(options);
72+
const cld = Cloudinary.new(snakeCaseOptions);
73+
74+
// Let cloudinary-core handle genrating this video tag attributes
75+
const tagAttributes = cld.videoTag(publicId, snakeCaseOptions).attributes();
76+
77+
// Aggregate child transformations, used for generating <source> tags for this video element
78+
const childTransformations = this.getTransformation({...options, children});
79+
80+
let sources = null;
4181

4282
if (Util.isArray(sourceTypes)) {
43-
sources = sourceTypes.map(srcType => {
44-
let sourceTransformation = sourceTransformations[srcType] || {};
45-
let src = cld.url(publicId, Util.defaults({}, sourceTransformation, childTransformations, {resource_type: 'video', format: srcType}));
46-
let mimeType = 'video/' + (srcType === 'ogv' ? 'ogg' : srcType);
47-
return <source key={mimeType} src={src} type={mimeType}/>;
48-
}
49-
);
83+
// We have multiple sourceTypes, so we generate <source> tags.
84+
sources = this.generateSources(cld, publicId, childTransformations, sourceTransformation, sourceTypes);
5085
} else {
51-
let sourceTransformation = sourceTransformations[sourceTypes] || {};
52-
tagAttributes.src = cld.url(publicId, Util.defaults({}, sourceTransformation, childTransformations, {resource_type: 'video', format: sourceTypes}));
86+
// We have a single source type so we generate the src attribute of this video element.
87+
tagAttributes.src = this.generateVideoUrl(cld, publicId, childTransformations, sourceTransformation, sourceTypes);
5388
}
5489

90+
return {sources, tagAttributes};
91+
};
92+
93+
/**
94+
* Render a video element
95+
*/
96+
render() {
97+
const {innerRef, fallback, children} = this.props;
98+
99+
const {
100+
tagAttributes, // Attributes of this video element
101+
sources // <source> tags of this video element
102+
} = this.getVideoTagProps();
103+
55104
return (
56-
<video ref={innerRef} {...tagAttributes}>
105+
<video
106+
ref={innerRef}
107+
{...tagAttributes}>
57108
{sources}
58109
{fallback}
59-
{this.props.children}
110+
{children}
60111
</video>
61112
);
62113
}
63114
}
115+
64116
Video.propTypes = {publicId: PropTypes.string};
65117
Video.defaultProps = {};
66-
Video.contextTypes = CloudinaryComponent.contextTypes;
67118

68119
export default Video;

test/VideoTest.js

Lines changed: 139 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,90 @@
11
import React from 'react';
2-
import chai, { expect } from 'chai';
2+
import chai, {expect} from 'chai';
33
import {shallow, mount} from 'enzyme';
44
import Video from '../src/components/Video';
55
import Transformation from '../src/components/Transformation';
66

77
chai.use(require('chai-string'));
88

99
describe('Video', () => {
10-
it("should include child transformation for a single source type", function () {
11-
let tag = shallow(
12-
<Video cloudName='demo'
13-
sourceTypes={'webm'}
14-
publicId='dog'
15-
sourceTransformation={{
16-
webm: {overlay: 'text:verdana_30:webm!'}
17-
}}>
18-
<Transformation quality='70'/>
19-
</Video>);
20-
expect(tag.props().src).to.endWith('/q_70/l_text:verdana_30:webm!/dog.webm');
21-
});
22-
23-
it("should support fps parameter", function () {
24-
let tag = shallow(
25-
<Video cloudName='demo'
26-
sourceTypes={'webm'}
27-
publicId='dog'>
28-
<Transformation fps='24'/>
29-
</Video>);
30-
expect(tag.props().src).to.endWith('/fps_24/dog.webm');
31-
tag = shallow(
32-
<Video cloudName='demo'
33-
sourceTypes={'webm'}
34-
publicId='dog'>
35-
<Transformation fps='24-29.97'/>
36-
</Video>);
37-
expect(tag.props().src).to.endWith('/fps_24-29.97/dog.webm');
38-
tag = shallow(
39-
<Video cloudName='demo'
40-
sourceTypes={'webm'}
41-
publicId='dog'>
42-
<Transformation fps='25-'/>
43-
</Video>);
44-
expect(tag.props().src).to.endWith('/fps_25-/dog.webm');
45-
});
46-
47-
it('should support startOffset parameter', function() {
48-
let tag = shallow(
49-
<Video cloudName="demo" sourceTypes={'webm'} publicId="dog">
50-
<Transformation startOffset="auto" />
51-
</Video>);
52-
expect(tag.props().src).to.endWith('/so_auto/dog.webm');
53-
tag = shallow(
54-
<Video cloudName="demo" sourceTypes={'webm'} publicId="dog">
55-
<Transformation startOffset="2" />
56-
</Video>);
57-
expect(tag.props().src).to.endWith('/so_2/dog.webm');
58-
tag = shallow(
59-
<Video cloudName="demo" sourceTypes={'webm'} publicId="dog">
60-
<Transformation startOffset="2.34" />
61-
</Video>);
62-
expect(tag.props().src).to.endWith('/so_2.34/dog.webm');
63-
});
64-
65-
it("should include child transformation for multiple source types", function () {
66-
let tag = shallow(
67-
<Video cloudName='demo'
68-
sourceTypes={['webm', 'mp4']}
69-
publicId='dog'
70-
sourceTransformation={{
71-
webm: {overlay: 'text:verdana_30:webm!'},
72-
mp4: {overlay: 'text:verdana_30:mp4!'}
73-
}}>
74-
<Transformation quality='70'/>
75-
</Video>);
76-
expect(tag.find('[type="video/webm"]').props().src).to.endWith('/q_70/l_text:verdana_30:webm!/dog.webm');
77-
expect(tag.find('[type="video/mp4"]').props().src).to.endWith('/q_70/l_text:verdana_30:mp4!/dog.mp4');
78-
});
79-
80-
it('should support inner text', function () {
81-
let tag = shallow(
82-
<Video cloudName='demo' publicId='dog'>
83-
Your browser does not support the video tag.
84-
</Video>
85-
);
86-
expect(tag.type()).to.equal("video");
87-
});
10+
it("should include child transformation for a single source type", function () {
11+
let tag = shallow(
12+
<Video cloudName='demo'
13+
sourceTypes={'webm'}
14+
publicId='dog'
15+
sourceTransformation={{
16+
webm: {overlay: 'text:verdana_30:webm!'}
17+
}}>
18+
<Transformation quality='70'/>
19+
</Video>);
20+
expect(tag.props().src).to.endWith('/q_70/l_text:verdana_30:webm!/dog.webm');
21+
});
22+
23+
it("should support fps parameter", function () {
24+
let tag = shallow(
25+
<Video cloudName='demo'
26+
sourceTypes={'webm'}
27+
publicId='dog'>
28+
<Transformation fps='24'/>
29+
</Video>);
30+
expect(tag.props().src).to.endWith('/fps_24/dog.webm');
31+
tag = shallow(
32+
<Video cloudName='demo'
33+
sourceTypes={'webm'}
34+
publicId='dog'>
35+
<Transformation fps='24-29.97'/>
36+
</Video>);
37+
expect(tag.props().src).to.endWith('/fps_24-29.97/dog.webm');
38+
tag = shallow(
39+
<Video cloudName='demo'
40+
sourceTypes={'webm'}
41+
publicId='dog'>
42+
<Transformation fps='25-'/>
43+
</Video>);
44+
expect(tag.props().src).to.endWith('/fps_25-/dog.webm');
45+
});
46+
47+
it('should support startOffset parameter', function () {
48+
let tag = shallow(
49+
<Video cloudName="demo" sourceTypes={'webm'} publicId="dog">
50+
<Transformation startOffset="auto"/>
51+
</Video>);
52+
expect(tag.props().src).to.endWith('/so_auto/dog.webm');
53+
tag = shallow(
54+
<Video cloudName="demo" sourceTypes={'webm'} publicId="dog">
55+
<Transformation startOffset="2"/>
56+
</Video>);
57+
expect(tag.props().src).to.endWith('/so_2/dog.webm');
58+
tag = shallow(
59+
<Video cloudName="demo" sourceTypes={'webm'} publicId="dog">
60+
<Transformation startOffset="2.34"/>
61+
</Video>);
62+
expect(tag.props().src).to.endWith('/so_2.34/dog.webm');
63+
});
64+
65+
it("should include child transformation for multiple source types", function () {
66+
let tag = shallow(
67+
<Video cloudName='demo'
68+
sourceTypes={['webm', 'mp4']}
69+
publicId='dog'
70+
sourceTransformation={{
71+
webm: {overlay: 'text:verdana_30:webm!'},
72+
mp4: {overlay: 'text:verdana_30:mp4!'}
73+
}}>
74+
<Transformation quality='70'/>
75+
</Video>);
76+
expect(tag.find('[type="video/webm"]').props().src).to.endWith('/q_70/l_text:verdana_30:webm!/dog.webm');
77+
expect(tag.find('[type="video/mp4"]').props().src).to.endWith('/q_70/l_text:verdana_30:mp4!/dog.mp4');
78+
});
79+
80+
it('should support inner text', function () {
81+
let tag = shallow(
82+
<Video cloudName='demo' publicId='dog'>
83+
Your browser does not support the video tag.
84+
</Video>
85+
);
86+
expect(tag.type()).to.equal("video");
87+
});
8888

8989
it('Should support forwarding innerRef to underlying video element', function () {
9090
let myRef = React.createRef();
@@ -105,6 +105,65 @@ describe('Video', () => {
105105

106106
expect(tag.find('video').prop('src')).to.endWith('/l_text:verdana_30:webm!/dog.webm');
107107
expect(video.src).to.endWith('/l_text:verdana_30:webm!/dog.webm');
108-
['play','pause','canPlayType','addTextTrack'].forEach(func=>expect(video[func]).to.be.a('function'));
108+
['play', 'pause', 'canPlayType', 'addTextTrack'].forEach(func => expect(video[func]).to.be.a('function'));
109+
});
110+
111+
it('Should set default video poster', function () {
112+
let tag = shallow(
113+
<Video cloudName='demo' publicId='dog'/>
114+
);
115+
116+
expect(tag.props().poster).to.equal('http://res.cloudinary.com/demo/video/upload/dog.jpg');
117+
});
118+
119+
it('Should set secure video poster', function () {
120+
let tag = shallow(
121+
<Video cloudName='demo' publicId='dog' poster={{secure: true}}/>
122+
);
123+
124+
expect(tag.props().poster).to.equal('https://res.cloudinary.com/demo/video/upload/dog.jpg');
125+
});
126+
127+
it('Should set video poster url', function () {
128+
let tag = shallow(
129+
<Video cloudName='demo' publicId='dog'
130+
poster={'https://res.cloudinary.com/demo/video/upload/w_200,dpr_2.0/dog.jpg'}/>
131+
);
132+
133+
expect(tag.props().poster).to.equal('https://res.cloudinary.com/demo/video/upload/w_200,dpr_2.0/dog.jpg');
134+
});
135+
136+
it('Should set video poster public_id and transformation', function () {
137+
const poster = {
138+
public_id: 'elephants',
139+
transformation: [
140+
{width: 100, crop: "scale"},
141+
{dpr: "2.0"}
142+
]
143+
};
144+
145+
let tag = shallow(
146+
<Video cloudName='demo' publicId='dog' poster={poster}/>
147+
);
148+
149+
expect(tag.props().poster).to.equal('http://res.cloudinary.com/demo/image/upload/c_scale,w_100/dpr_2.0/elephants');
150+
});
151+
152+
it('Should set video poster public_id, resource_type and transformation', function () {
153+
const poster = {
154+
public_id: 'elephants',
155+
resource_type: 'video',
156+
format: 'jpg',
157+
transformation: [
158+
{width: 100, crop: "scale"},
159+
{dpr: "2.0"}
160+
]
161+
};
162+
163+
let tag = shallow(
164+
<Video cloudName='demo' publicId='dog' poster={poster}/>
165+
);
166+
167+
expect(tag.props().poster).to.equal('http://res.cloudinary.com/demo/video/upload/c_scale,w_100/dpr_2.0/elephants.jpg');
109168
});
110169
});

0 commit comments

Comments
 (0)