diff --git a/CHANGELOG.md b/CHANGELOG.md index 81611d93..928aeb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +1.9.15-edge.1 / 2023-08-24 +================== + +* Support camelCase cloud config (i.e. cloudName) + +1.9.15-edge.0 / 2023-08-16 +================== + + + 1.9.14 / 2023-08-16 ================== diff --git a/docs/index.html b/docs/index.html index f4cd1189..e51537a1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -21,7 +21,7 @@ diff --git a/package.json b/package.json index 20174062..932ce975 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloudinary-video-player", - "version": "1.9.14", + "version": "1.9.15-edge.1", "description": "Cloudinary Video Player", "author": "Cloudinary", "license": "MIT", @@ -65,7 +65,9 @@ }, "dependencies": { "@cloudinary/url-gen": "^1.10.2", + "cloudinary-video-analytics": "^1.2.0", "dashjs": "^4.7.1", + "lodash": "^4.17.21", "uuid": "^9.0.0", "video.js": "8.3.0", "videojs-contrib-ads": "^6.9.0", diff --git a/src/components/cld-analytics/cld-analytics.js b/src/components/cld-analytics/cld-analytics.js deleted file mode 100644 index bfdce47a..00000000 --- a/src/components/cld-analytics/cld-analytics.js +++ /dev/null @@ -1,106 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { sendBeaconRequest } from './send-beacon-request'; -import { VIDEO_EVENT } from './events.consts'; - -const CLD_ANALYTICS_ENDPOINT_URL = 'https://video-analytics-api.cloudinary.com/video-analytics'; -const CLD_ANALYTICS_USER_ID_KEY = 'cld-analytics-user-id'; - -const getUniqueUserId = () => { - const storageUserId = window.localStorage.getItem(CLD_ANALYTICS_USER_ID_KEY); - - if (storageUserId) { - return storageUserId; - } - - const userId = uuidv4(); - window.localStorage.setItem(CLD_ANALYTICS_USER_ID_KEY, userId); - return userId; -}; - -// prepare events list for aggregation, for example -// if video is being played and user wants to leave the page - add "pause" event to correctly calculate played time -const prepareEvents = (collectedEvents) => { - const events = [...collectedEvents]; - const lastPlayItemIndex = events.findLastIndex((event) => event.type === VIDEO_EVENT.PLAY); - const lastPauseItemIndex = events.findLastIndex((event) => event.type === VIDEO_EVENT.PAUSE); - - if (lastPlayItemIndex > lastPauseItemIndex) { - events.push({ - type: VIDEO_EVENT.PAUSE, - time: Date.now() - }); - } - - return events; -}; - -const aggregateEvents = (eventsList) => { - return eventsList.reduce((acc, event) => { - const lastItem = acc.watchedFrames[acc.watchedFrames.length - 1]; - - if (event.type === VIDEO_EVENT.PLAY) { - acc.watchedFrames.push([event.time]); - } else if (lastItem && lastItem.length === 1 && event.type === VIDEO_EVENT.PAUSE) { - lastItem.push(event.time); - } - - return acc; - }, { - watchedFrames: [] - }); -}; - -const getPlayedTimeSeconds = (watchedFrames) => { - return Math.round(watchedFrames.reduce((acc, [playTime, pauseTime]) => { - return acc + ((pauseTime - playTime) / 1000); - }, 0)); -}; - -export const trackVideoPlayer = (videoElement, metadataProps) => { - const collectedEvents = []; - - window.addEventListener('beforeunload', () => { - const videoCurrentTime = videoElement.currentTime; - const videoDuration = videoElement.duration; - const events = prepareEvents(collectedEvents, videoCurrentTime); - const { watchedFrames } = aggregateEvents(events); - const playedTimeSeconds = getPlayedTimeSeconds(watchedFrames); - - // video public id is registered later in videojs, so we need to postpone public id fetching - const metadata = typeof metadataProps === 'function' ? metadataProps() : metadataProps; - - // temporary solution to prevent sending invalid data - if ([undefined, 'undefined', null, ''].includes(metadata.cloudName)) { - return; - } - - sendBeaconRequest(CLD_ANALYTICS_ENDPOINT_URL, { - ...metadata, - userId: getUniqueUserId(), - videoDuration, - playedTimeSeconds - }); - }); - - videoElement.addEventListener('play', () => { - collectedEvents.push({ - type: VIDEO_EVENT.PLAY, - time: Date.now() - }); - }); - - videoElement.addEventListener('pause', () => { - collectedEvents.push({ - type: VIDEO_EVENT.PAUSE, - time: Date.now() - }); - }); - - videoElement.addEventListener('emptied', () => { - // simulate "pause" event when source is changed - collectedEvents.push({ - type: VIDEO_EVENT.PAUSE, - time: Date.now() - }); - }); -}; diff --git a/src/components/cld-analytics/events.consts.js b/src/components/cld-analytics/events.consts.js deleted file mode 100644 index d8833ee1..00000000 --- a/src/components/cld-analytics/events.consts.js +++ /dev/null @@ -1,4 +0,0 @@ -export const VIDEO_EVENT = { - PLAY: 'play', - PAUSE: 'pause' -}; diff --git a/src/components/cld-analytics/send-beacon-request.js b/src/components/cld-analytics/send-beacon-request.js deleted file mode 100644 index 7ea96f97..00000000 --- a/src/components/cld-analytics/send-beacon-request.js +++ /dev/null @@ -1,17 +0,0 @@ -export const sendBeaconRequest = (url, data) => { - const params = Object.keys(data).reduce((formData, key) => { - formData.append(key, data[key]); - return formData; - }, new FormData()); - - if (typeof window.navigator.sendBeacon !== 'function') { - return fetch(url, { - method: 'POST', - mode: 'no-cors', - body: params, - keepalive: true - }); - } - - return window.navigator.sendBeacon(url, params); -}; diff --git a/src/index.js b/src/index.js index 7039c419..d3b2c60d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,22 +1,31 @@ import 'assets/styles/main.scss'; import VideoPlayer from './video-player'; import { assign } from 'utils/assign'; -import { omit, pick } from './utils/object'; +import { pick, convertKeysToSnakeCase } from './utils/object'; import { CLOUDINARY_CONFIG_PARAM } from './video-player.const'; if (window.cloudinary && window.cloudinary.Cloudinary) { - console.warn('For version 1.9.0 and above, cloudinary-core is not needed for using the Cloudinary Video Player'); + console.warn( + 'For version 1.9.0 and above, cloudinary-core is not needed for using the Cloudinary Video Player' + ); } -const getConfig = (playerOptions = {}, cloudinaryConfig) => assign( - omit(playerOptions, CLOUDINARY_CONFIG_PARAM), - { cloudinaryConfig: cloudinaryConfig || pick(playerOptions, CLOUDINARY_CONFIG_PARAM) }); +const getConfig = (playerOptions = {}, cloudinaryConfig) => { + const snakeCaseCloudinaryConfig = pick(convertKeysToSnakeCase(playerOptions), CLOUDINARY_CONFIG_PARAM); -const getVideoPlayer = (config) => (id, playerOptions, ready) => new VideoPlayer(id, getConfig(playerOptions, config), ready); + // pick cld-configurations and assign them to cloudinaryConfig + return assign(playerOptions, { + cloudinaryConfig: cloudinaryConfig || snakeCaseCloudinaryConfig + }); +}; + +const getVideoPlayer = config => (id, playerOptions, ready) => + new VideoPlayer(id, getConfig(playerOptions, config), ready); -const getVideoPlayers = (config) => (selector, playerOptions, ready) => VideoPlayer.all(selector, getConfig(playerOptions, config), ready); +const getVideoPlayers = config => (selector, playerOptions, ready) => + VideoPlayer.all(selector, getConfig(playerOptions, config), ready); -const cloudinaryVideoPlayerConfig = (config) => ({ +const cloudinaryVideoPlayerConfig = config => ({ videoPlayer: getVideoPlayer(config), videoPlayers: getVideoPlayers(config) }); @@ -36,4 +45,4 @@ const cloudinary = { window.cloudinary = cloudinary; -export default cloudinary; \ No newline at end of file +export default cloudinary; diff --git a/src/plugins/cloudinary-analytics/index.js b/src/plugins/cloudinary-analytics/index.js index 4e9b0470..8340bc7b 100644 --- a/src/plugins/cloudinary-analytics/index.js +++ b/src/plugins/cloudinary-analytics/index.js @@ -1,19 +1,24 @@ -import { trackVideoPlayer } from '../../components/cld-analytics/cld-analytics'; +import connectCloudinaryAnalytics from 'cloudinary-video-analytics'; +import { PLAYER_EVENT } from '../../utils/consts'; class CloudinaryAnalytics { constructor(player) { this.player = player; + this.cloudinaryAnalytics = connectCloudinaryAnalytics(this.player.videoElement); } - getMetadata = () => { - return { - cloudName: this.player.cloudinary.cloudinaryConfig().cloud_name, - videoPublicId: this.player.cloudinary.currentPublicId() - }; - } + getMetadata = () => ({ + cloudName: this.player.cloudinary.cloudinaryConfig().cloud_name, + publicId: this.player.cloudinary.currentPublicId() + }) init() { - trackVideoPlayer(this.player.videoElement, this.getMetadata); + this.player.on(PLAYER_EVENT.SOURCE_CHANGED, () => { + const metadata = this.getMetadata(); + if (metadata.cloudName && metadata.publicId) { + this.cloudinaryAnalytics.startManuallyNewVideoTracking(metadata); + } + }); } } diff --git a/src/utils/object.js b/src/utils/object.js index f61670a6..4de5c354 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -1,3 +1,4 @@ +import snakeCase from 'lodash/snakeCase'; /** * a nested value from an object @@ -25,7 +26,6 @@ export const get = (value, path, defaultValue) => { return defaultValue; }; - export const pick = (obj, keys) => { return keys.reduce((acc, key) => { const value = obj[key]; @@ -49,3 +49,14 @@ export const omit = (obj, keys) => { return acc; }, {}); }; + +export const convertKeysToSnakeCase = (obj) => { + let snakeCaseObj = {}; + + for (const key of Object.keys(obj)) { + const snakeCaseKey = snakeCase(key); + snakeCaseObj[snakeCaseKey] = obj[key]; + } + + return snakeCaseObj; +}; diff --git a/src/utils/string.js b/src/utils/string.js index a22ea144..a26b0bdb 100644 --- a/src/utils/string.js +++ b/src/utils/string.js @@ -1,4 +1,3 @@ - function camelize(str) { return str.replace(/[_.-](\w|$)/g, (_, x) => x.toUpperCase()); } diff --git a/src/video-player.js b/src/video-player.js index 8f34eac7..1d22ee66 100644 --- a/src/video-player.js +++ b/src/video-player.js @@ -61,6 +61,10 @@ class VideoPlayer extends Utils.mixin(Eventable) { }; } + get playerOptions() { + return this.options.playerOptions; + } + constructor(elem, initOptions, ready) { super(); @@ -516,10 +520,6 @@ class VideoPlayer extends Utils.mixin(Eventable) { return this.videojs.cloudinary.cloudinaryConfig(config); } - get playerOptions() { - return this.options.playerOptions; - } - currentPublicId() { return this.videojs.cloudinary.currentPublicId(); } diff --git a/yarn.lock b/yarn.lock index 3a7ef0c5..2666d3f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2858,6 +2858,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +cloudinary-video-analytics@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cloudinary-video-analytics/-/cloudinary-video-analytics-1.2.0.tgz#18583dd34bb59b981c8178179d521754a88c9540" + integrity sha512-Iif0hzAapvfhbO7IACQh9VmJmHZsCyzN0IgvZpNtw6cSK4h9Xc5rlG72C1gf/hCwZzVHXU0UrRh8you9xtfKMQ== + dependencies: + uuid "9.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -9094,6 +9101,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@9.0.0, uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -9104,11 +9116,6 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== - v8-to-istanbul@^7.0.0: version "7.1.2" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"