From 0e7d74a5e9b7bb3495228355f57084c692b4b6f4 Mon Sep 17 00:00:00 2001 From: Romaric MOYEUVRE Date: Mon, 9 Dec 2024 15:39:33 +0100 Subject: [PATCH] MediaVideo, BackgroundVideo --- addons/website/static/src/core/video_utils.js | 45 +++ .../interactions/video/background_video.js | 112 +++++++ .../src/interactions/video/media_video.js | 91 ++++++ .../src/js/content/snippets.animation.js | 306 ------------------ 4 files changed, 248 insertions(+), 306 deletions(-) create mode 100644 addons/website/static/src/core/video_utils.js create mode 100644 addons/website/static/src/interactions/video/background_video.js create mode 100644 addons/website/static/src/interactions/video/media_video.js diff --git a/addons/website/static/src/core/video_utils.js b/addons/website/static/src/core/video_utils.js new file mode 100644 index 0000000000000..e48720a34073a --- /dev/null +++ b/addons/website/static/src/core/video_utils.js @@ -0,0 +1,45 @@ +import { loadJS } from "@web/core/assets"; +import { hasTouch } from "@web/core/browser/feature_detection"; +import { SIZES, utils as uiUtils } from "@web/core/ui/ui_service"; + +/** + * Takes care of any necessary setup for autoplaying video. In practice, + * this method will load the youtube iframe API for mobile environments + * because mobile environments don't support the youtube autoplay param + * passed in the url. + * + * @param {string} src + * @param {boolean} needCookiesApproval + */ +export function setupAutoplay(src, needCookiesApproval = false) { + const isYoutubeVideo = src.indexOf('youtube') >= 0; + const isMobileEnv = uiUtils.getSize() <= SIZES.LG && hasTouch(); + + if (isYoutubeVideo && isMobileEnv && !window.YT && !needCookiesApproval) { + const oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady; + const promise = new Promise(resolve => { + window.onYouTubeIframeAPIReady = () => { + if (oldOnYoutubeIframeAPIReady) { + oldOnYoutubeIframeAPIReady(); + } + return resolve(); + }; + }); + loadJS('https://www.youtube.com/iframe_api'); + return promise; + } + return null; +} + +/** + * @param {HTMLIframeElement} iframeEl - the iframe containing the video player + */ +export function triggerAutoplay(iframeEl) { + // YouTube does not allow to auto-play video in mobile devices, so we + // have to play the video manually. + new window.YT.Player(iframeEl, { + events: { + onReady: ev => ev.target.playVideo(), + } + }); +} diff --git a/addons/website/static/src/interactions/video/background_video.js b/addons/website/static/src/interactions/video/background_video.js new file mode 100644 index 0000000000000..072c9cb848377 --- /dev/null +++ b/addons/website/static/src/interactions/video/background_video.js @@ -0,0 +1,112 @@ +import { Interaction } from "@website/core/interaction"; +import { registry } from "@web/core/registry"; + +import { _t } from "@web/core/l10n/translation"; +import { uniqueId } from "@web/core/utils/functions"; +import { renderToElement } from "@web/core/utils/render"; + +import { setupAutoplay, triggerAutoplay } from "../../core/video_utils"; + +class BackgroundVideo extends Interaction { + static selector = ".o_background_video"; + dynamicSelectors = { + ...this.dynamicSelectors, + _dropdown: () => this.el.closest(".dropdown-menu").parentElement, + _modal: () => this.el.closest("modal"), + } + dynamicContent = { + _document: { + "t-on-optionalCookiesAccepted": () => this.iframeEl.src = this.videoSrc, + }, + _window: { + "t-on-resize": this.throttledForAnimation(() => this.adjustIframe()), + }, + _dropdown: { + "t-on-shown.bs.dropdown": this.throttledForAnimation(() => this.adjustIframe()), + }, + _modal: { + "t-on-show.bs.modal": () => this.hideVideoContainer = true, + "t-on-shown.bs.modal": () => this.hideVideoContainer = false, + }, + ".o_bg_video_container": { + "t-att-class": () => ({ + "d-none": this.hideVideoContainer, + }), + }, + } + + setup() { + this.hideVideoContainer = false; + this.videoSrc = this.el.dataset.bgVideoSrc; + this.iframeID = uniqueId("o_bg_video_iframe_"); + } + + start() { + const promise = setupAutoplay(this.videoSrc); + if (promise) { + this.videoSrc += "&enablejsapi=1"; + this.waitFor(promise).then(() => this.appendBgVideo()); + } + } + + adjustIframe() { + if (!this.iframeEl) { + return; + } + + this.iframeEl.classList.remove("show"); + + var wrapperWidth = this.el.innerWidth; + var wrapperHeight = this.el.innerHeight; + var relativeRatio = (wrapperWidth / wrapperHeight) / (16 / 9); + + if (relativeRatio >= 1.0) { + this.iframeEl.style.width = "100%"; + this.iframeEl.style.height = (relativeRatio * 100) + "%"; + this.iframeEl.style.insetInlineStart = "0"; + this.iframeEl.style.insetBlockStart = (-(relativeRatio - 1.0) / 2 * 100) + "%"; + } else { + this.iframeEl.style.width = ((1 / relativeRatio) * 100) + "%"; + this.iframeEl.style.height = "100%"; + this.iframeEl.style.insetInlineStart = (-((1 / relativeRatio) - 1.0) / 2 * 100) + "%"; + this.iframeEl.style.insetBlockStart = "0"; + } + + void this.iframeEl.offsetWidth; // Force style addition + this.iframeEl.classList.add("show"); + } + + appendBgVideo() { + const allowedCookies = !this.el.dataset.needCookiesApproval; + + const oldContainer = this.bgVideoContainer || this.querySelector(":scope > .o_bg_video_container"); + this.bgVideoContainer = renderToElement("website.background.video", { + videoSrc: allowedCookies ? this.videoSrc : "about:blank", + iframeID: this.iframeID, + }); + + this.iframeEl = this.bgVideoContainer.querySelector(".o_bg_video_iframe"); + this.addListener(this.iframe, "load", () => { + this.bgVideoContainer.find(".o_bg_video_loading").remove(); + // When there is a "slide in (left or right) animation" element, we + // need to adjust the iframe size once it has been loaded, otherwise + // an horizontal scrollbar may appear. + this.adjustIframe(); + }); + this.insert(this.bgVideoContainer, this.el, "afterbegin") + oldContainer.remove(); + + this.adjustIframe(); + triggerAutoplay(this.iframeEl); + } +} + +registry + .category("website.active_elements") + .add("website.background_video", BackgroundVideo); + +registry + .category("website.editable_active_elements_builders") + .add("website.background_video", { + Interaction: BackgroundVideo, + }); diff --git a/addons/website/static/src/interactions/video/media_video.js b/addons/website/static/src/interactions/video/media_video.js new file mode 100644 index 0000000000000..32f18454e83b9 --- /dev/null +++ b/addons/website/static/src/interactions/video/media_video.js @@ -0,0 +1,91 @@ +import { Interaction } from "@website/core/interaction"; +import { registry } from "@web/core/registry"; + +import { _t } from "@web/core/l10n/translation"; +import { escape } from "@web/core/utils/strings"; + +import { setupAutoplay, triggerAutoplay } from "../../core/video_utils"; + +class MediaVideo extends Interaction { + static selector = ".media_iframe_video"; + + setup() { + if (this.el.dataset.needCookiesApproval) { + this.sizeContainerEl = this.el.querySelector(":scope > .media_iframe_video_size"); + this.sizeContainerEl.classList.add("d-none"); + this.addListener(document, "optionalCookiesAccepted", this.sizeContainerEl.classList.remove("d-none")) + this.registerCleanup(() => this.sizeContainerEl.classList.remove("d-none")); + } + } + + start() { + let iframeEl = this.el.querySelector(':scope > iframe'); + + // The following code is only there to ensure compatibility with + // videos added before bug fixes or new Odoo versions where the + //