Skip to content

Commit 228cc1f

Browse files
author
RTLcoil
authored
Added support for sources in video tag (#265)
(Update video HTML 5 tag to include support for h265 and vp9)
1 parent 1ff578d commit 228cc1f

File tree

6 files changed

+234
-48
lines changed

6 files changed

+234
-48
lines changed

lib-es5/cloudinary.js

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ exports.video = function video(public_id, options) {
128128
public_id = public_id.replace(/\.(mp4|ogv|webm)$/, '');
129129
var source_types = optionConsume(options, 'source_types', []);
130130
var source_transformation = optionConsume(options, 'source_transformation', {});
131+
var sources = optionConsume(options, 'sources', []);
131132
var fallback = optionConsume(options, 'fallback_content', '');
132133

133134
if (source_types.length === 0) source_types = cloudinary.utils.DEFAULT_VIDEO_SOURCE_TYPES;
@@ -150,31 +151,35 @@ exports.video = function video(public_id, options) {
150151
var html = '<video ';
151152

152153
if (!video_options.hasOwnProperty('resource_type')) video_options.resource_type = 'video';
153-
var multi_source = _.isArray(source_types) && source_types.length > 1;
154+
var multi_source_types = _.isArray(source_types) && source_types.length > 1;
155+
var has_sources = _.isArray(sources) && sources.length > 0;
154156
var source = public_id;
155-
if (!multi_source) {
157+
if (!multi_source_types && !has_sources) {
156158
source = source + '.' + cloudinary.utils.build_array(source_types)[0];
157159
}
158-
var src = cloudinary.utils.url(source, video_options); // calculate src and reduce video_options
159-
if (!multi_source) {
160-
video_options.src = src;
161-
}
160+
var src = cloudinary.utils.url(source, video_options);
161+
if (!multi_source_types && !has_sources) video_options.src = src;
162162
if (video_options.hasOwnProperty("html_width")) video_options.width = optionConsume(video_options, 'html_width');
163163
if (video_options.hasOwnProperty("html_height")) video_options.height = optionConsume(video_options, 'html_height');
164164
html = html + cloudinary.utils.html_attrs(video_options) + '>';
165-
if (multi_source) {
166-
html += source_types.map(function (source_type) {
167-
var transformation = source_transformation[source_type] || {};
168-
var sourceSrc = cloudinary.utils.url(source + "." + source_type, _.extend({ resource_type: 'video' }, _.cloneDeep(options), _.cloneDeep(transformation)));
169-
var video_type = source_type === 'ogv' ? 'ogg' : source_type;
170-
var type = "video/" + video_type;
171-
return `<source ${cloudinary.utils.html_attrs({ src: sourceSrc, type })}>`;
165+
if (multi_source_types && !has_sources) {
166+
sources = source_types.map(function (source_type) {
167+
return {
168+
type: source_type,
169+
transformations: source_transformation[source_type] || {}
170+
};
171+
});
172+
}
173+
if (_.isArray(sources) && sources.length > 0) {
174+
html += sources.map(function (source_data) {
175+
var source_type = source_data.type;
176+
var codecs = source_data.codecs;
177+
var transformation = source_data.transformations || {};
178+
src = cloudinary.utils.url(source + "." + source_type, _.extend({ resource_type: 'video' }, _.cloneDeep(options), _.cloneDeep(transformation)));
179+
return cloudinary.utils.create_source_tag(src, source_type, codecs);
172180
}).join('');
173181
}
174-
175-
html = html + fallback;
176-
html = html + '</video>';
177-
return html;
182+
return `${html}${fallback}</video>`;
178183
};
179184

180185
/**

lib-es5/utils/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,18 @@ function archive_params() {
12791279
};
12801280
}
12811281

1282+
exports.create_source_tag = function create_source_tag(src, source_type) {
1283+
var codecs = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
1284+
1285+
var video_type = source_type === 'ogv' ? 'ogg' : source_type;
1286+
var mime_type = `video/${video_type}`;
1287+
if (!isEmpty(codecs)) {
1288+
var codecs_str = isArray(codecs) ? codecs.join(', ') : codecs;
1289+
mime_type += `; codecs=${codecs_str}`;
1290+
}
1291+
return `<source ${utils.html_attrs({ src, type: mime_type })}>`;
1292+
};
1293+
12821294
function build_explicit_api_params(public_id) {
12831295
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
12841296

lib/cloudinary.js

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ exports.video = function video(public_id, options) {
118118
public_id = public_id.replace(/\.(mp4|ogv|webm)$/, '');
119119
let source_types = optionConsume(options, 'source_types', []);
120120
let source_transformation = optionConsume(options, 'source_transformation', {});
121+
let sources = optionConsume(options, 'sources', []);
121122
let fallback = optionConsume(options, 'fallback_content', '');
122123

123124
if (source_types.length === 0) source_types = cloudinary.utils.DEFAULT_VIDEO_SOURCE_TYPES;
@@ -128,53 +129,45 @@ exports.video = function video(public_id, options) {
128129
if (video_options.poster.hasOwnProperty('public_id')) {
129130
video_options.poster = cloudinary.utils.url(video_options.poster.public_id, video_options.poster);
130131
} else {
131-
video_options.poster = cloudinary.utils.url(
132-
public_id,
133-
_.extend(
134-
{},
135-
cloudinary.utils.DEFAULT_POSTER_OPTIONS,
136-
video_options.poster,
137-
),
138-
);
132+
video_options.poster = cloudinary.utils.url(public_id, _.extend({}, cloudinary.utils.DEFAULT_POSTER_OPTIONS, video_options.poster));
139133
}
140134
}
141135
} else {
142-
video_options.poster = cloudinary.utils.url(
143-
public_id,
144-
_.extend({}, cloudinary.utils.DEFAULT_POSTER_OPTIONS, options),
145-
);
136+
video_options.poster = cloudinary.utils.url(public_id, _.extend({}, cloudinary.utils.DEFAULT_POSTER_OPTIONS, options));
146137
}
147138

148139
if (!video_options.poster) delete video_options.poster;
149140

150141
let html = '<video ';
151142

152143
if (!video_options.hasOwnProperty('resource_type')) video_options.resource_type = 'video';
153-
let multi_source = _.isArray(source_types) && source_types.length > 1;
144+
let multi_source_types = _.isArray(source_types) && source_types.length > 1;
145+
let has_sources = _.isArray(sources) && sources.length > 0;
154146
let source = public_id;
155-
if (!multi_source) {
147+
if (!multi_source_types && !has_sources) {
156148
source = source + '.' + cloudinary.utils.build_array(source_types)[0];
157149
}
158-
let src = cloudinary.utils.url(source, video_options); // calculate src and reduce video_options
159-
if (!multi_source) {
160-
video_options.src = src;
161-
}
150+
let src = cloudinary.utils.url(source, video_options);
151+
if (!multi_source_types && !has_sources) video_options.src = src;
162152
if (video_options.hasOwnProperty("html_width")) video_options.width = optionConsume(video_options, 'html_width');
163153
if (video_options.hasOwnProperty("html_height")) video_options.height = optionConsume(video_options, 'html_height');
164154
html = html + cloudinary.utils.html_attrs(video_options) + '>';
165-
if (multi_source) {
166-
html += source_types.map((source_type) => {
167-
let transformation = source_transformation[source_type] || {};
168-
let sourceSrc = cloudinary.utils.url(source + "." + source_type, _.extend({ resource_type: 'video' }, _.cloneDeep(options), _.cloneDeep(transformation)));
169-
let video_type = source_type === 'ogv' ? 'ogg' : source_type;
170-
let type = "video/" + video_type;
171-
return `<source ${cloudinary.utils.html_attrs({ src: sourceSrc, type })}>`;
155+
if (multi_source_types && !has_sources) {
156+
sources = source_types.map(source_type => ({
157+
type: source_type,
158+
transformations: source_transformation[source_type] || {},
159+
}));
160+
}
161+
if (_.isArray(sources) && sources.length > 0) {
162+
html += sources.map((source_data) => {
163+
let source_type = source_data.type;
164+
let codecs = source_data.codecs;
165+
let transformation = source_data.transformations || {};
166+
src = cloudinary.utils.url(source + "." + source_type, _.extend({ resource_type: 'video' }, _.cloneDeep(options), _.cloneDeep(transformation)));
167+
return cloudinary.utils.create_source_tag(src, source_type, codecs);
172168
}).join('');
173169
}
174-
175-
html = html + fallback;
176-
html = html + '</video>';
177-
return html;
170+
return `${html}${fallback}</video>`;
178171
};
179172

180173

lib/utils/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,16 @@ function archive_params(options = {}) {
11761176
};
11771177
}
11781178

1179+
exports.create_source_tag = function create_source_tag(src, source_type, codecs = null) {
1180+
let video_type = source_type === 'ogv' ? 'ogg' : source_type;
1181+
let mime_type = `video/${video_type}`;
1182+
if (!isEmpty(codecs)) {
1183+
let codecs_str = isArray(codecs) ? codecs.join(', ') : codecs;
1184+
mime_type += `; codecs=${codecs_str}`;
1185+
}
1186+
return `<source ${utils.html_attrs({ src, type: mime_type })}>`;
1187+
};
1188+
11791189
function build_explicit_api_params(public_id, options = {}) {
11801190
return [exports.build_upload_params(extend({}, { public_id }, options))];
11811191
}

test/spechelper.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,28 @@ exports.ICON_FILE = "test/resources/favicon.ico";
3939
exports.VIDEO_URL = "http://res.cloudinary.com/demo/video/upload/dog.mp4";
4040
exports.IMAGE_URL = "http://res.cloudinary.com/demo/image/upload/sample";
4141

42-
exports.test_cloudinary_url = function (public_id, options, expected_url, expected_options) {
42+
exports.SAMPLE_VIDEO_SOURCES = [
43+
{
44+
type: 'mp4',
45+
codecs: 'hev1',
46+
transformations: { video_codec: 'h265' },
47+
},
48+
{
49+
type: 'webm',
50+
codecs: 'vp9',
51+
transformations: { video_codec: 'vp9' },
52+
},
53+
{
54+
type: 'mp4',
55+
transformations: { video_codec: 'auto' },
56+
},
57+
{
58+
type: 'webm',
59+
transformations: { video_codec: 'auto' },
60+
},
61+
];
62+
63+
exports.test_cloudinary_url = function(public_id, options, expected_url, expected_options) {
4364
var url;
4465
url = utils.url(public_id, options);
4566
expect(url).to.eql(expected_url);

test/video_spec.js

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
var cloudinary, expect;
1+
var cloudinary, expect, helper;
22

33
expect = require('expect.js');
44

55
cloudinary = require('../cloudinary');
66

7+
helper = require("./spechelper");
8+
79
describe("video tag helper", function () {
810
var DEFAULT_UPLOAD_PATH, VIDEO_UPLOAD_PATH;
911
VIDEO_UPLOAD_PATH = "http://res.cloudinary.com/test123/video/upload/";
@@ -154,4 +156,147 @@ describe("video tag helper", function () {
154156
expect(options.video_codec).to.eql('auto');
155157
expect(options.autoplay).to.be(true);
156158
});
159+
160+
describe('sources', function() {
161+
const expected_url = VIDEO_UPLOAD_PATH + 'movie';
162+
const expected_url_mp4 = VIDEO_UPLOAD_PATH + 'vc_auto/movie.mp4';
163+
const expected_url_webm = VIDEO_UPLOAD_PATH + 'vc_auto/movie.webm';
164+
it('should generate video tag with default sources if not given sources or source_types', function() {
165+
expect(cloudinary.video('movie')).to.eql(
166+
`<video poster='${expected_url}.jpg'>` +
167+
`<source src='${expected_url}.webm' type='video/webm'>` +
168+
`<source src='${expected_url}.mp4' type='video/mp4'>` +
169+
`<source src='${expected_url}.ogv' type='video/ogg'>` +
170+
'</video>'
171+
);
172+
});
173+
it('should generate video tag with given custom sources', function() {
174+
var custom_sources = [
175+
{
176+
type: 'mp4',
177+
},
178+
{
179+
type: 'webm',
180+
},
181+
];
182+
expect(
183+
cloudinary.video('movie', {
184+
sources: custom_sources,
185+
})
186+
).to.eql(
187+
`<video poster='${expected_url}.jpg'>` +
188+
`<source src='${VIDEO_UPLOAD_PATH +
189+
'movie.mp4'}' type='video/mp4'>` +
190+
`<source src='${VIDEO_UPLOAD_PATH +
191+
'movie.webm'}' type='video/webm'>` +
192+
`</video>`
193+
);
194+
});
195+
it('should generate video tag overriding source_types with sources if both are given', function() {
196+
var custom_sources = [
197+
{
198+
type: 'mp4',
199+
},
200+
];
201+
expect(
202+
cloudinary.video('movie', {
203+
sources: custom_sources,
204+
source_types: ['ogv', 'mp4', 'webm'],
205+
})
206+
).to.eql(
207+
`<video poster='${expected_url}.jpg'>` +
208+
`<source src='${VIDEO_UPLOAD_PATH + 'movie.mp4'}' type='video/mp4'>` +
209+
`</video>`
210+
);
211+
});
212+
it('should correctly handle ogg/ogv', function() {
213+
expect(
214+
cloudinary.video('movie', {
215+
sources: [{ type: 'ogv' }],
216+
})
217+
).to.eql(
218+
`<video poster='${expected_url}.jpg'>` +
219+
`<source src='${VIDEO_UPLOAD_PATH +
220+
'movie.ogv'}' type='video/ogg'>` +
221+
`</video>`
222+
);
223+
});
224+
it('should generate video tag with sources with codecs string', function() {
225+
var custom_sources = [
226+
{
227+
type: 'mp4',
228+
codecs: 'vp8, vorbis',
229+
transformations: { video_codec: 'auto' },
230+
},
231+
{
232+
type: 'webm',
233+
codecs: 'avc1.4D401E, mp4a.40.2',
234+
transformations: { video_codec: 'auto' },
235+
},
236+
];
237+
expect(
238+
cloudinary.video('movie', {
239+
sources: custom_sources,
240+
})
241+
).to.eql(
242+
`<video poster='${expected_url}.jpg'>` +
243+
`<source src='${expected_url_mp4}' type='video/mp4; codecs=vp8, vorbis'>` +
244+
`<source src='${expected_url_webm}' type='video/webm; codecs=avc1.4D401E, mp4a.40.2'>` +
245+
`</video>`
246+
);
247+
});
248+
it('should generate video tag with sources with codecs arrays', function() {
249+
var custom_sources = [
250+
{
251+
type: 'mp4',
252+
codecs: ['vp8', 'vorbis'],
253+
transformations: { video_codec: 'auto' },
254+
},
255+
{
256+
type: 'webm',
257+
codecs: ['avc1.4D401E', 'mp4a.40.2'],
258+
transformations: { video_codec: 'auto' },
259+
},
260+
];
261+
expect(
262+
cloudinary.video('movie', {
263+
sources: custom_sources,
264+
})
265+
).to.eql(
266+
`<video poster='${expected_url}.jpg'>` +
267+
`<source src='${expected_url_mp4}' type='video/mp4; codecs=vp8, vorbis'>` +
268+
`<source src='${expected_url_webm}' type='video/webm; codecs=avc1.4D401E, mp4a.40.2'>` +
269+
`</video>`
270+
);
271+
});
272+
it('should generate video tag with sources and transformations', function() {
273+
const options = {
274+
source_types: 'mp4',
275+
html_height: '100',
276+
html_width: '200',
277+
video_codec: { codec: 'h264' },
278+
audio_codec: 'acc',
279+
start_offset: 3,
280+
sources: helper.SAMPLE_VIDEO_SOURCES,
281+
};
282+
const expected_poster_url =
283+
VIDEO_UPLOAD_PATH + 'ac_acc,so_3,vc_h264/movie.jpg';
284+
const expected_url_mp4_codecs =
285+
VIDEO_UPLOAD_PATH + 'ac_acc,so_3,vc_h265/movie.mp4';
286+
const expected_url_webm_codecs =
287+
VIDEO_UPLOAD_PATH + 'ac_acc,so_3,vc_vp9/movie.webm';
288+
const expected_url_mp4_audio =
289+
VIDEO_UPLOAD_PATH + 'ac_acc,so_3,vc_auto/movie.mp4';
290+
const expected_url_webm_audio =
291+
VIDEO_UPLOAD_PATH + 'ac_acc,so_3,vc_auto/movie.webm';
292+
expect(cloudinary.video('movie', options)).to.eql(
293+
`<video height='100' poster='${expected_poster_url}' width='200'>` +
294+
`<source src='${expected_url_mp4_codecs}' type='video/mp4; codecs=hev1'>` +
295+
`<source src='${expected_url_webm_codecs}' type='video/webm; codecs=vp9'>` +
296+
`<source src='${expected_url_mp4_audio}' type='video/mp4'>` +
297+
`<source src='${expected_url_webm_audio}' type='video/webm'>` +
298+
`</video>`
299+
);
300+
});
301+
});
157302
});

0 commit comments

Comments
 (0)