Skip to content

Commit 522058a

Browse files
Merge pull request #195 from cloudinary/SNI-6952-react-video-cld-poster
Sni 6952 add video cld poster
2 parents d95301d + fcbc593 commit 522058a

File tree

16 files changed

+146
-32
lines changed

16 files changed

+146
-32
lines changed

packages/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@angular/cli": "~10.2.3",
3434
"@angular/compiler-cli": "~10.2.4",
3535
"@cloudinary/html": "^1.8.1",
36-
"@cloudinary/url-gen": "^1.8.6",
36+
"@cloudinary/url-gen": "^1.8.7",
3737
"@types/jasmine": "~3.5.0",
3838
"@types/jasminewd2": "~2.0.3",
3939
"@types/node": "^12.11.1",

packages/angular/playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@angular/platform-browser": "~10.2.4",
1616
"@angular/platform-browser-dynamic": "~10.2.4",
1717
"@angular/router": "~10.2.4",
18-
"@cloudinary/url-gen": "~1.8.7",
18+
"@cloudinary/url-gen": "^1.8.7",
1919
"rxjs": "~6.6.0",
2020
"tslib": "^2.0.0",
2121
"zone.js": "~0.10.2"

packages/angular/projects/cloudinary-library/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"@angular/core": ">=10.0.0"
88
},
99
"devDependencies": {
10-
"@cloudinary/url-gen": "^1.8.6"
10+
"@cloudinary/url-gen": "^1.8.7"
1111
},
1212
"dependencies": {
1313
"@cloudinary/html": "^1.8.1"

packages/angular/projects/cloudinary-library/src/lib/cloudinary-video.component.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {Component, OnInit, Input, ElementRef, EventEmitter, Output, OnChanges, OnDestroy} from '@angular/core';
22
import {CloudinaryVideo} from '@cloudinary/url-gen';
3+
34
import {
45
cancelCurrentlyRunningPlugins,
56
HtmlVideoLayer,
67
Plugins,
8+
VideoPoster,
79
VideoSources
810
} from '@cloudinary/html';
911

@@ -13,6 +15,7 @@ import {
1315
* @type {Component}
1416
* @description The Cloudinary video component.
1517
* @prop {CloudinaryVideo} transformation Generated by @cloudinary/url-gen
18+
* @prop {VideoPoster} transformation Generated by @cloudinary/url-gen
1619
* @prop {Plugins} plugins Advanced image component plugins lazyload()
1720
* @prop videoAttributes Optional attributes include controls, loop, muted, poster, preload, autoplay
1821
* @prop videoEvents Optional video events include play, loadstart, playing, error, ended
@@ -50,6 +53,7 @@ export class CloudinaryVideoComponent implements OnInit, OnChanges, OnDestroy {
5053
constructor(private el: ElementRef) { }
5154

5255
@Input('cldVid') cldVid: CloudinaryVideo;
56+
@Input('cldPoster') cldPoster: VideoPoster;
5357
@Input('sources') sources: VideoSources;
5458
@Input('plugins') plugins: Plugins;
5559
@Input('poster') poster: string;
@@ -82,7 +86,8 @@ export class CloudinaryVideoComponent implements OnInit, OnChanges, OnDestroy {
8286
this.cldVid,
8387
this.sources,
8488
this.plugins,
85-
this.getVideoAttributes()
89+
this.getVideoAttributes(),
90+
this.cldPoster
8691
);
8792

8893
// check if video should be muted. We need to take care of this here since Angular has a bug with binding the muted
@@ -102,7 +107,7 @@ export class CloudinaryVideoComponent implements OnInit, OnChanges, OnDestroy {
102107
ngOnChanges() {
103108
if (this.htmlVideoLayerInstance) {
104109
cancelCurrentlyRunningPlugins(this.htmlVideoLayerInstance.htmlPluginState);
105-
this.htmlVideoLayerInstance.update(this.cldVid, this.sources, this.plugins, this.getVideoAttributes());
110+
this.htmlVideoLayerInstance.update(this.cldVid, this.sources, this.plugins, this.getVideoAttributes(), this.cldPoster);
106111
}
107112
}
108113

packages/angular/projects/cloudinary-library/src/tests/cloudinary-video.component.spec.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
22
import { CloudinaryVideoComponent } from '../lib/cloudinary-video.component';
3-
import {CloudinaryVideo} from '@cloudinary/url-gen';
3+
import {CloudinaryImage, CloudinaryVideo} from '@cloudinary/url-gen';
44
import { auto, vp9 } from '@cloudinary/url-gen/qualifiers/videoCodec';
55
import { videoCodec } from '@cloudinary/url-gen/actions/transcode';
66
import {ElementRef} from "@angular/core";
77

8+
const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
89
const cloudinaryVideo = new CloudinaryVideo('sample', { cloudName: 'demo'}, { analytics: false });
910

1011
describe('CloudinaryVideoComponent render', () => {
@@ -73,7 +74,6 @@ describe('CloudinaryVideoComponent render', () => {
7374
tick(0);
7475
const vidElement: HTMLVideoElement = fixture.nativeElement;
7576
const video = vidElement.querySelector('video');
76-
const defaultVideoTypes = ['webm', 'mp4', 'ogv'];
7777

7878
expect(video.childElementCount).toBe(2);
7979

@@ -90,6 +90,31 @@ describe('CloudinaryVideoComponent render', () => {
9090
.toEqual( 'video/webm; codecs=avc1.4D401E, mp4a.40.2');
9191
}));
9292

93+
94+
it('should contain poster when "auto" is passed as cldPoster', fakeAsync(() => {
95+
component.cldVid = new CloudinaryVideo('sample', { cloudName: 'demo'}, { analytics: false });
96+
component.cldPoster = "auto";
97+
const vidElement: HTMLVideoElement = fixture.nativeElement;
98+
const video = vidElement.querySelector('video');
99+
fixture.detectChanges();
100+
tick(0);
101+
102+
expect(video.attributes.getNamedItem('poster').value)
103+
.toEqual( 'https://res.cloudinary.com/demo/video/upload/q_auto/f_jpg/so_auto/sample');
104+
}));
105+
106+
it('should contain poster when cloudinary image is passed as cldPoster', fakeAsync(() => {
107+
component.cldVid = cloudinaryVideo;
108+
component.cldPoster = cloudinaryImage;
109+
const vidElement: HTMLVideoElement = fixture.nativeElement;
110+
const video = vidElement.querySelector('video');
111+
fixture.detectChanges();
112+
tick(0);
113+
114+
expect(video.attributes.getNamedItem('poster').value)
115+
.toEqual( 'https://res.cloudinary.com/demo/image/upload/sample');
116+
}));
117+
93118
it('should emit playing event', fakeAsync(() => {
94119
component.cldVid = cloudinaryVideo;
95120
fixture.detectChanges();

packages/html/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"devDependencies": {
1818
"@babel/preset-env": "^7.14.7",
1919
"@babel/preset-typescript": "^7.14.5",
20-
"@cloudinary/url-gen": "^1.8.6",
20+
"@cloudinary/url-gen": "^1.8.7",
2121
"@rollup/plugin-commonjs": "^19.0.0",
2222
"@rollup/plugin-node-resolve": "^13.0.0",
2323
"core-js": "^3.23.5",

packages/html/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export {accessibility} from './plugins/accessibility.js';
99
export {placeholder} from './plugins/placeholder.js';
1010
export {isBrowser} from './utils/isBrowser.js';
1111
export {serverSideSrc} from './utils/serverSideSrc.js';
12-
export {Plugins, VideoSources, PictureSources} from './types.js';
12+
export {Plugins, VideoSources, VideoPoster, PictureSources} from './types.js';
1313
export {cancelCurrentlyRunningPlugins} from './utils/cancelCurrentlyRunningPlugins.js';

packages/html/src/layers/htmlVideoLayer.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Plugins, HtmlPluginState, VideoSources, VideoType} from '../types.js'
1+
import {Plugins, HtmlPluginState, VideoSources, VideoType, VideoPoster} from '../types.js'
22
import cloneDeep from 'lodash.clonedeep'
33
import {CloudinaryVideo} from "@cloudinary/url-gen";
44
import {render} from '../utils/render.js';
@@ -13,7 +13,7 @@ export class HtmlVideoLayer{
1313
mimeType = 'video';
1414
mimeSubTypes = VIDEO_MIME_TYPES;
1515

16-
constructor(element: HTMLVideoElement | null, userCloudinaryVideo: CloudinaryVideo, sources: VideoSources, plugins?: Plugins, videoAttributes?: object){
16+
constructor(element: HTMLVideoElement | null, userCloudinaryVideo: CloudinaryVideo, sources: VideoSources, plugins?: Plugins, videoAttributes?: object, userCloudinaryPoster?: VideoPoster){
1717
this.videoElement = element;
1818
this.originalVideo = userCloudinaryVideo;
1919
this.htmlPluginState = {cleanupCallbacks:[], pluginEventSubscription: []};
@@ -23,7 +23,7 @@ export class HtmlVideoLayer{
2323
.then(()=>{ // when resolved updates sources
2424
this.htmlPluginState.pluginEventSubscription.forEach(fn=>{fn()});
2525

26-
this.setVideoAttributes(videoAttributes);
26+
this.setVideoAttributes(videoAttributes, userCloudinaryPoster);
2727
this.handleSourceToVideo(pluginCloudinaryVideo, sources)
2828
});
2929

@@ -104,14 +104,22 @@ export class HtmlVideoLayer{
104104
* In case of poster, sets the poster.
105105
* @param videoAttributes {object} Supported attributes: controls, loop, muted, poster, preload, autoplay, playsinline
106106
*/
107-
setVideoAttributes(videoAttributes: object) {
108-
if (videoAttributes) {
109-
for (const [key, value] of Object.entries(videoAttributes)) {
110-
// Boolean attributes are considered to be true if they're present on the element at all.
111-
// You should set value to the empty string ("") or the attribute's name.
112-
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute
113-
value && this.videoElement.setAttribute(key, key === 'poster' ? value : '');
114-
}
107+
setVideoAttributes(videoAttributes: object = {}, userCloudinaryPoster?: VideoPoster) {
108+
if (userCloudinaryPoster === 'auto') {
109+
const posterCloudinaryVideo = cloneDeep(this.originalVideo);
110+
videoAttributes['poster'] = posterCloudinaryVideo
111+
.quality('auto')
112+
.format('jpg')
113+
.addTransformation('so_auto')
114+
.toURL()
115+
} else if (userCloudinaryPoster) {
116+
videoAttributes['poster'] = userCloudinaryPoster.toURL?.();
117+
}
118+
for (const [key, value] of Object.entries(videoAttributes)) {
119+
// Boolean attributes are considered to be true if they're present on the element at all.
120+
// You should set value to the empty string ("") or the attribute's name.
121+
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute
122+
value && this.videoElement.setAttribute(key, key === 'poster' ? value : '');
115123
}
116124
}
117125

@@ -122,14 +130,14 @@ export class HtmlVideoLayer{
122130
* @param plugins
123131
* @param videoAttributes
124132
*/
125-
update(updatedCloudinaryVideo: CloudinaryVideo, sources: VideoSources, plugins?: Plugins, videoAttributes?: object){
133+
update(updatedCloudinaryVideo: CloudinaryVideo, sources: VideoSources, plugins?: Plugins, videoAttributes?: object, userCloudinaryPoster?: VideoPoster){
126134
if(updatedCloudinaryVideo !== this.originalVideo){
127135
const sourcesToDelete = this.videoElement.getElementsByTagName("SOURCE");
128136
while (sourcesToDelete[0]) sourcesToDelete[0].parentNode.removeChild(sourcesToDelete[0]);
129137

130138
render(this.videoElement, updatedCloudinaryVideo, plugins, this.htmlPluginState)
131139
.then(()=>{ // when resolved updates sources
132-
this.setVideoAttributes(videoAttributes);
140+
this.setVideoAttributes(videoAttributes, userCloudinaryPoster);
133141
this.handleSourceToVideo(updatedCloudinaryVideo, sources);
134142
this.videoElement.load();
135143
});

packages/html/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ export type PictureSources = {minWidth?: number, maxWidth?: number, image: Cloud
2020
export type PictureSource = {minWidth?: number, maxWidth?: number, image: CloudinaryImage, sizes?: string};
2121

2222
export type AnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string};
23+
24+
export type VideoPoster = CloudinaryImage | 'auto';

packages/react/__tests__/AdvancedVideo.test.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { AdvancedVideo } from '../src';
2-
import { CloudinaryVideo } from '@cloudinary/url-gen';
2+
import { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';
33
import { mount } from 'enzyme';
44
import React from 'react';
55
import { auto, vp9 } from '@cloudinary/url-gen/qualifiers/videoCodec';
66
import { videoCodec } from '@cloudinary/url-gen/actions/transcode';
77

8+
const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
89
const cloudinaryVideo = new CloudinaryVideo('sample', { cloudName: 'demo' }, { analytics: false });
910
const cloudinaryVideoWithAnalytics = new CloudinaryVideo('sample', { cloudName: 'demo' }, { analytics: true });
1011

@@ -80,6 +81,24 @@ describe('AdvancedVideo', () => {
8081
}, 0);// one tick
8182
});
8283

84+
it('should contain poster when "auto" is passed as cldPoster', function (done) {
85+
const component = mount(<AdvancedVideo cldVid={cloudinaryVideo} cldPoster="auto" />);
86+
87+
setTimeout(() => {
88+
expect(component.html()).toContain('poster="https://res.cloudinary.com/demo/video/upload/q_auto/f_jpg/so_auto/sample"');
89+
done();
90+
}, 0);// one tick
91+
});
92+
93+
it('should contain poster when cloudinary image is passed as cldPoster', function (done) {
94+
const component = mount(<AdvancedVideo cldVid={cloudinaryVideo} cldPoster={cloudinaryImage} />);
95+
96+
setTimeout(() => {
97+
expect(component.html()).toContain('poster="https://res.cloudinary.com/demo/image/upload/sample"');
98+
done();
99+
}, 0);// one tick
100+
});
101+
83102
it('should simulate onPlay event', function (done) {
84103
const mockCallBack = jest.fn();
85104

0 commit comments

Comments
 (0)