Skip to content
Merged
1,144 changes: 581 additions & 563 deletions dist/player.es.js

Large diffs are not rendered by default.

1,144 changes: 581 additions & 563 deletions dist/player.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/player.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/player.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/player.min.js.map

Large diffs are not rendered by default.

116 changes: 78 additions & 38 deletions src/lib/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import Player from '../player';
import { isVimeoUrl, isVimeoEmbed, getVimeoUrl, getOembedDomain } from './functions';
import { isVimeoUrl, getVimeoUrl, getOembedDomain, isVimeoEmbed, findIframeBySourceWindow } from './functions';
import { parseMessageData } from './postmessage';

const oEmbedParameters = [
Expand Down Expand Up @@ -224,19 +224,13 @@ export function resizeEmbeds(parent = document) {
return;
}

const iframes = parent.querySelectorAll('iframe');

for (let i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow !== event.source) {
continue;
}
const senderIFrame = event.source ? findIframeBySourceWindow(event.source, parent) : null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const senderIFrame = event.source ? findIframeBySourceWindow(event.source, parent) : null;
const senderIFrame = findIframeBySourceWindow(event.source, parent);

Nit. findIframeBySourceWindow already returns null if event.source is falsy.


if (senderIFrame) {
// Change padding-bottom of the enclosing div to accommodate
// card carousel without distorting aspect ratio
const space = iframes[i].parentElement;
const space = senderIFrame.parentElement;
space.style.paddingBottom = `${event.data.data[0].bottom}px`;

break;
}
};

Expand Down Expand Up @@ -266,16 +260,12 @@ export function initAppendVideoMetadata(parent = document) {
return;
}

const iframes = parent.querySelectorAll('iframe');
for (let i = 0; i < iframes.length; i++) {
const iframe = iframes[i];
const senderIFrame = event.source ? findIframeBySourceWindow(event.source, parent) : null;

// Initiate appendVideoMetadata if iframe is a Vimeo embed
const isValidMessageSource = iframe.contentWindow === event.source;
if (isVimeoEmbed(iframe.src) && isValidMessageSource) {
const player = new Player(iframe);
player.callMethod('appendVideoMetadata', window.location.href);
}
// Initiate appendVideoMetadata if iframe is a Vimeo embed
if (senderIFrame && isVimeoEmbed(senderIFrame.src)) {
const player = new Player(senderIFrame);
player.callMethod('appendVideoMetadata', window.location.href);
}
};

Expand Down Expand Up @@ -311,25 +301,75 @@ export function checkUrlTimeParam(parent = document) {
return;
}

const iframes = parent.querySelectorAll('iframe');
for (let i = 0; i < iframes.length; i++) {
const iframe = iframes[i];
const isValidMessageSource = iframe.contentWindow === event.source;

if (isVimeoEmbed(iframe.src) && isValidMessageSource) {
const player = new Player(iframe);
player
.getVideoId()
.then((videoId) => {
const matches = new RegExp(`[?&]vimeo_t_${videoId}=([^&#]*)`).exec(window.location.href);
if (matches && matches[1]) {
const sec = decodeURI(matches[1]);
player.setCurrentTime(sec);
}
return;
})
.catch(handleError);
}
const senderIFrame = event.source ? findIframeBySourceWindow(event.source, parent) : null;

if (senderIFrame && isVimeoEmbed(senderIFrame.src)) {
const player = new Player(senderIFrame);
player
.getVideoId()
.then((videoId) => {
const matches = new RegExp(`[?&]vimeo_t_${videoId}=([^&#]*)`).exec(window.location.href);
if (matches && matches[1]) {
const sec = decodeURI(matches[1]);
player.setCurrentTime(sec);
}
return;
})
.catch(handleError);
}
};

window.addEventListener('message', onMessage);
}


/**
* Updates iframe embeds to support DRM content playback by adding the 'encrypted-media' permission
* to the iframe's allow attribute when DRM initialization fails. This function acts as a fallback
* mechanism to enable playback of DRM-protected content in embeds that weren't properly configured.
*
* @return {void}
*/
export function updateDRMEmbeds() {
if (window.VimeoDRMEmbedsUpdated) {
return;
}
window.VimeoDRMEmbedsUpdated = true;

/**
* Handle message events for DRM initialization failures
* @param {MessageEvent} event - The message event from the iframe
*/
const onMessage = (event) => {
if (!isVimeoUrl(event.origin)) {
return;
}

const data = parseMessageData(event.data);
if (!data || data.event !== 'drminitfailed') {
return;
}

const senderIFrame = event.source ? findIframeBySourceWindow(event.source) : null;

if (!senderIFrame) {
return;
}

const currentAllow = senderIFrame.getAttribute('allow') || '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const currentAllow = senderIFrame.getAttribute('allow') || '';
const currentAllow = senderIFrame.allow;

Nit. allow lets you retrieve this a little easier.

const allowSupportsDRM = currentAllow.includes('encrypted-media');

if (!allowSupportsDRM) {
// For DRM playback to successfully occur, the iframe `allow` attribute must include 'encrypted-media'.
// If the video requires DRM but doesn't have the attribute, we try to add on behalf of the embed owner
// as a temporary measure to enable playback until they're able to update their embeds.
senderIFrame.setAttribute('allow', `${currentAllow}; encrypted-media`);
const currentUrl = new URL(senderIFrame.getAttribute('src'));

// Adding this forces the embed to reload once `allow` has been updated with `encrypted-media`.
currentUrl.searchParams.set('forcereload', 'drm');
senderIFrame.setAttribute('src', currentUrl.toString());
return;
}
};

Expand Down
23 changes: 23 additions & 0 deletions src/lib/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,26 @@ export const logSurveyLink = () => {
'color:#aaa;font-size:0.8em;'
);
};

/**
* Find the iframe element that contains a specific source window
*
* @param {Window} sourceWindow The source window to find the iframe for
* @param {Document} [doc=document] The document to search within
* @return {HTMLIFrameElement|null} The iframe element if found, otherwise null
*/
export function findIframeBySourceWindow(sourceWindow, doc = document) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (!sourceWindow || !doc || typeof doc.querySelectorAll !== 'function') {
return null;
}

const iframes = doc.querySelectorAll('iframe');

for (let i = 0; i < iframes.length; i++) {
if (iframes[i] && iframes[i].contentWindow === sourceWindow) {
return iframes[i];
}
}

return null;
}
4 changes: 3 additions & 1 deletion src/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
initializeEmbeds,
resizeEmbeds,
initAppendVideoMetadata,
checkUrlTimeParam
checkUrlTimeParam,
updateDRMEmbeds
} from './lib/embed';
import { parseMessageData, postMessage, processData } from './lib/postmessage';
import { initializeScreenfull } from './lib/screenfull.js';
Expand Down Expand Up @@ -1369,6 +1370,7 @@ if (!isServerRuntime) {
initAppendVideoMetadata();
checkUrlTimeParam();
logSurveyLink();
updateDRMEmbeds();
}

export default Player;
60 changes: 59 additions & 1 deletion test/embed-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import test from 'ava';
import html from './helpers/html';
import { getOEmbedParameters, getOEmbedData, createEmbed, initializeEmbeds, resizeEmbeds, initAppendVideoMetadata } from '../src/lib/embed';
import { triggerMessageHandler, createDRMInitFailedEvent } from './helpers/message-handler';
import { getOEmbedParameters, getOEmbedData, createEmbed, initializeEmbeds, resizeEmbeds, initAppendVideoMetadata, updateDRMEmbeds } from '../src/lib/embed';

test('getOEmbedParameters retrieves the params from data attributes', (t) => {
const el = html`<div data-vimeo-id="445351154" data-vimeo-width="640" data-vimeo-autoplay></div>`;
Expand Down Expand Up @@ -92,3 +93,60 @@ test('initAppendVideoMetadata is a function and sets a window property', (t) =>
initAppendVideoMetadata();
t.true(window.VimeoSeoMetadataAppended);
});

test('updateDRMEmbeds is a function and sets a window property', (t) => {
t.plan(2);
t.true(typeof updateDRMEmbeds === 'function');

updateDRMEmbeds();
t.true(window.VimeoDRMEmbedsUpdated);
});

test('updateDRMEmbeds adds encrypted-media to allow attribute when DRM init fails', (t) => {
window.VimeoDRMEmbedsUpdated = false;

const iframe = html`<iframe src="https://player.vimeo.com/video/123456" allow="autoplay"></iframe>`;
document.body.appendChild(iframe);

const mockEvent = createDRMInitFailedEvent('https://player.vimeo.com', iframe.contentWindow);
triggerMessageHandler(() => updateDRMEmbeds(), mockEvent);

t.true(iframe.getAttribute('allow').includes('encrypted-media'), 'encrypted-media was added to allow attribute');
t.true(iframe.getAttribute('src').includes('forcereload='), 'forcereload parameter was added to src');

document.body.removeChild(iframe);
});

test('updateDRMEmbeds does not modify iframe if encrypted-media is already present', (t) => {
window.VimeoDRMEmbedsUpdated = false;

const iframe = html`<iframe src="https://player.vimeo.com/video/123456" allow="autoplay; encrypted-media"></iframe>`;
document.body.appendChild(iframe);

const originalSrc = iframe.getAttribute('src');

const mockEvent = createDRMInitFailedEvent('https://player.vimeo.com', iframe.contentWindow);
triggerMessageHandler(() => updateDRMEmbeds(), mockEvent);

t.is(iframe.getAttribute('src'), originalSrc, 'Source URL was not modified');

document.body.removeChild(iframe);
});

test('updateDRMEmbeds ignores messages from non-Vimeo origins', (t) => {
window.VimeoDRMEmbedsUpdated = false;

const iframe = html`<iframe src="https://player.vimeo.com/video/123456" allow="autoplay"></iframe>`;
document.body.appendChild(iframe);

const originalSrc = iframe.getAttribute('src');
const originalAllow = iframe.getAttribute('allow');

const mockEvent = createDRMInitFailedEvent('https://not-vimeo.com', iframe.contentWindow);
triggerMessageHandler(() => updateDRMEmbeds(), mockEvent);

t.is(iframe.getAttribute('src'), originalSrc, 'Source URL was not modified');
t.is(iframe.getAttribute('allow'), originalAllow, 'Allow attribute was not modified');

document.body.removeChild(iframe);
});
68 changes: 67 additions & 1 deletion test/functions-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'ava';
import html from './helpers/html';
import { getMethodName, isDomElement, isInteger, isVimeoUrl, isVimeoEmbed, getVimeoUrl, getOembedDomain } from '../src/lib/functions';
import { getMethodName, isDomElement, isInteger, isVimeoUrl, isVimeoEmbed, getVimeoUrl, getOembedDomain, findIframeBySourceWindow } from '../src/lib/functions';

test('getMethodName properly formats the method name', (t) => {
t.true(getMethodName('color', 'get') === 'getColor');
Expand Down Expand Up @@ -112,3 +112,69 @@ test('getVimeoUrl throws when the required keys don’t exist', (t) => {
getVimeoUrl({ url: 'https://notvimeo.com/2' });
}, { instanceOf: TypeError });
});

test('findIframeBySourceWindow returns the correct iframe for a given source window', (t) => {
// Create a mock document with iframes
const mockDoc = {
iframes: [],
querySelectorAll: function(selector) {
if (selector === 'iframe') {
return this.iframes;
}
return [];
}
};

// Create mock iframes with contentWindow properties
const mockWindow1 = {};
const mockWindow2 = {};
const mockWindow3 = {};

const mockIframe1 = { contentWindow: mockWindow1 };
const mockIframe2 = { contentWindow: mockWindow2 };
const mockIframe3 = { contentWindow: mockWindow3 };

mockDoc.iframes = [mockIframe1, mockIframe2, mockIframe3];

// Test finding each iframe by its source window
t.is(findIframeBySourceWindow(mockWindow1, mockDoc), mockIframe1);
t.is(findIframeBySourceWindow(mockWindow2, mockDoc), mockIframe2);
t.is(findIframeBySourceWindow(mockWindow3, mockDoc), mockIframe3);

// Test with a window that doesn't match any iframe
const unknownWindow = {};
t.is(findIframeBySourceWindow(unknownWindow, mockDoc), null);
});

test('findIframeBySourceWindow handles edge cases and invalid inputs', (t) => {
// Create a minimal mock document
const mockDoc = {
iframes: [],
querySelectorAll: function(selector) {
if (selector === 'iframe') {
return this.iframes;
}
return [];
}
};

// Test with null/undefined inputs
t.is(findIframeBySourceWindow(null, mockDoc), null);
t.is(findIframeBySourceWindow(undefined, mockDoc), null);
t.is(findIframeBySourceWindow({}, null), null);
t.is(findIframeBySourceWindow({}, undefined), null);

// Test with a document that doesn't have querySelectorAll
t.is(findIframeBySourceWindow({}, {}), null);

// Test with empty iframe array
t.is(findIframeBySourceWindow({}, mockDoc), null);

// Test with iframe that has null contentWindow
mockDoc.iframes = [{ contentWindow: null }];
t.is(findIframeBySourceWindow({}, mockDoc), null);

// Test with iframe that is null
mockDoc.iframes = [null];
t.is(findIframeBySourceWindow({}, mockDoc), null);
});
45 changes: 45 additions & 0 deletions test/helpers/message-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Helper function to trigger a message handler for testing
* This allows testing of functions that add message event listeners to the window
* without having to mock the entire MessageEvent infrastructure
*
* @param {Function} setupListener - Function that registers a message event handler (e.g., updateDRMEmbeds)
* @param {Object} mockEvent - The mock event object to pass to the handler
*/
export function triggerMessageHandler(setupListener, mockEvent) {
const originalAddEventListener = window.addEventListener;

let messageHandler;

window.addEventListener = function(eventName, handler) {
if (eventName === 'message') {
messageHandler = handler;
}
return originalAddEventListener.apply(this, arguments);
};

setupListener();

window.addEventListener = originalAddEventListener;

if (messageHandler) {
messageHandler(mockEvent);
}
}

/**
* Creates a mock DRM initialization failure event
*
* @param {string} origin - The origin of the message
* @param {Window} sourceWindow - The source window (iframe.contentWindow)
* @returns {Object} - A mock message event object
*/
export function createDRMInitFailedEvent(origin, sourceWindow) {
return {
origin,
source: sourceWindow,
data: JSON.stringify({
event: 'drminitfailed'
})
};
}