plyr/src/js/plugins/vimeo.js
Sam Potts 31861bd475 Merge branch 'master' into develop
# Conflicts:
#	package.json
#	src/js/plugins/vimeo.js
#	src/js/plugins/youtube.js
#	src/js/ui.js
#	yarn.lock
2020-11-14 13:11:24 +11:00

415 lines
11 KiB
JavaScript

// ==========================================================================
// Vimeo plugin
// ==========================================================================
import captions from '../captions';
import controls from '../controls';
import ui from '../ui';
import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadScript from '../utils/load-script';
import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = {
setup() {
const player = this;
// Add embed class for responsive
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set intial ratio
setAspectRatio.call(player);
// Load the SDK if not already
if (!is.object(window.Vimeo)) {
loadScript(player.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(player);
})
.catch((error) => {
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(player);
}
},
// API Ready
ready() {
const player = this;
const config = player.config.vimeo;
const { premium, referrerPolicy, ...frameParams } = config;
// If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false,
});
}
// Get Vimeo params for the iframe
const params = buildUrlParams({
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
...frameParams,
});
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture');
// Set the referrer policy if required
if (!is.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', referrerPolicy);
}
// Inject the package
if (premium || !config.customControls) {
iframe.setAttribute('data-poster', player.poster);
player.media = replaceElement(iframe, player.media);
} else {
const wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': player.poster,
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
// Get poster image
if (!config.customControls) {
fetch(format(player.config.urls.vimeo.api, src)).then((response) => {
if (is.empty(response) || !response.thumbnail_url) {
return;
}
// Set and show poster
ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
});
}
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
player.media.paused = true;
player.media.currentTime = 0;
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
player.pause();
player.currentTime = 0;
};
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(() => {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
});
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Loop
let { loop } = player.config;
Object.defineProperty(player.media, 'loop', {
get() {
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
});
},
});
// Source
let currentSrc;
player.embed
.getVideoUrl()
.then((value) => {
currentSrc = value;
controls.setDownloadUrl.call(player);
})
.catch((error) => {
this.debug.warn(error);
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {
const [width, height] = dimensions;
player.embed.ratio = [width, height];
setAspectRatio.call(this);
});
// Set autopause
player.embed.setAutopause(player.config.autopause).then((state) => {
player.config.autopause = state;
});
// Get title
player.embed.getVideoTitle().then((title) => {
player.config.title = title;
ui.setTitle.call(this);
});
// Get current time
player.embed.getCurrentTime().then((value) => {
currentTime = value;
triggerEvent.call(player, player.media, 'timeupdate');
});
// Get duration
player.embed.getDuration().then((value) => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Get captions
player.embed.getTextTracks().then((tracks) => {
player.media.textTracks = tracks;
captions.setup.call(player);
});
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map((cue) => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
player.embed.getPaused().then((paused) => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
// Fix keyboard focus issues
// https://github.com/sampotts/plyr/issues/317
frame.setAttribute('tabindex', -1);
}
});
player.embed.on('bufferstart', () => {
triggerEvent.call(player, player.media, 'waiting');
});
player.embed.on('bufferend', () => {
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => {
assurePlaybackState.call(player, false);
});
player.embed.on('timeupdate', (data) => {
player.media.seeking = false;
currentTime = data.seconds;
triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', (data) => {
player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'progress');
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then((value) => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', (detail) => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
if (config.customControls) {
setTimeout(() => ui.build.call(player), 0);
}
},
};
export default vimeo;