star back image
people4
電飾 電飾
moon
men
shopifyで動画プレイヤーを設置したい

【shopify】<video>動画プレイヤーを設置したい by vimeo

BLOG shopifyWEBログ
読了約:43分

vimeoやyoutube動画を表示させようとすると、iframeであったりyoutube APIであったり、動画のIDを読み込ませプレイヤーを表示させるでしょう。vimeoなら以下のような感じでしょうか。

<section>
  <div class="vimeo-movie-set">
    <div class="relative">
    <div class="product-vimeo">
      <iframe src="https://player.vimeo.com/video/{{ section.settings.vimeo_id }}?autoplay=1&loop=1&color=000&title=0&byline=0&portrait=0&muted=1&controls=1&autopause=0" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
    </div>
    <script src="https://player.vimeo.com/api/player.js"></script>
  </div>
</section>

ところがそうではない。
オリジナルのシンプルなプレーヤーにしたい。

vimeoロゴとか、snsのリンクアイコンとかいらないと言うのです。

そうなるともう作るしかない?

動画IDの読み込ませで作ったのに。。

便利なjsライブラリを利用する

どこかの案件で利用したプレイヤー.jsをいただいて利用しよう!
そういうことになり、すぐに実行です。

時間もアレですしお寿司。

今回のライブラリを拝見して新しく分かったことが、<video>タグで動画を表示させるのにvimeoのダウンロードURLを利用しているということでした。

vimeoダウンロード、Starterプラン以上で利用可能
https://vimeo.com/jp/features/video-editor/download-video

vimeoだと常識?私は知りませんでした。
そのダウンロードURLをshopifyのカスタマイズで登録して使います。

vimeoのダウンロードURLを利用する

ライブラリを利用してのオリジナルプレイヤーはとても早く実装できました。
完璧だと思っていたのですが、しかし。

しかし!?

複数の動画を登録したら、再生停止や、音の切り替えのボタンのコントロール操作できないと言うのです。ウソぉ。

1ページ内に複数の動画を入れると途端に、2つ目からでもプレイヤーで動画をコントロールできません。1つに減らすと正常に動作します。

複数の動画登録はテストしましたか?

してなかったかも。ダメなんですね。

AIに意見を聞いたところ、複数に対応するには「プレイヤーと動画を一致させる必要がある」と言うことでした。

なるほどぉ。よくわかりません。

AIと一緒に改修をしていく

無料なので低姿勢にAIさんへのらりくらりと質問すること1日。
AIさんは親切で前向きで、馬鹿にしたりしません。本当にありがたいです。
できたものを以下に共有します。

要件は以下です。

  • 自動再生でループさせておく。
  • PCとスマホで動画を切り替えたい。
  • スマホの動画がない時はPCの動画を表示させる。
  • 音は入り切り、シークバーとフルスクリーンは入れたい。

テーマhtml(footer.liquid )

<!-- vimeo set -->
<link rel="stylesheet" href="{{ 'player.css' | asset_url }}">
<script src="{{ 'player.js' | asset_url }}"></script>

スクリプトは1回呼ばれれば十分なので、テーマテンプレートのどこかに一箇所仕込みます。(同じスクリプトが複数呼ばれると不具合の原因に)

html(section-video-keymovie.liquid)

<!-- /sections/section-video-keymovie.liquid -->
<style>
.video-custom {
}
.fadeIn1s {
  animation-name: fadeIn1s;
  animation-delay: .5s;
  animation-duration: 1.0s;
  animation-fill-mode: forwards;
  opacity: 0;
}
@keyframes fadeIn1s {
  0% {
  }
  100% {
    opacity: 1;
  }
}
.kv .custom_video_wrapper {
  position: relative;
  height: 0;
  width: 100%;
  padding: 59.26% 0 0 0;
  background-color: #000;
}
.kv .custom_video_wrapper video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

@media only screen and (max-width: 1024px) {
  .kv .custom_video_wrapper {
    padding: 177.77% 0 0 0;
    height: auto;
    background-color: #000;
  }
}
</style>

<section class="video-custom">
  <div class="kv"
    data-section-id="{{ section.id }}"
    data-section-type="featured-video"
    data-overlay-header>
    {% if section.settings.vimeo_controls %}
      {% if section.settings.vimeo_url != blank %}
        <div class="custom_video_wrapper">
          <div class="custom_video_inner">
            <div class="video-container" data-video-id="{{ section.id }}" data-pc-video-url="{{ section.settings.vimeo_url }}" data-mobile-video-url="{{ section.settings.vimeo_url_mobile }}">
              <video id="video-{{ section.id }}" class="custom-video" autoplay playsinline muted loop allowfullscreen>
                <source type="video/mp4" src="">
              </video>
              <div class="video-controls fadeIn1s">
                <button class="video-play">
                  <img src="{{ 'pause.svg' | asset_url }}" alt="play video" width="12" height="12" title="play video">
                  <img src="{{ 'play.svg' | asset_url }}" alt="pause video" width="12" height="12" aria-hidden="true">
                </button>
                <div class="video-timeline">
                  <progress class="video-progressBar" value="0" min="0" max="100"></progress>
                  <input class="video-seek" value="0" min="0" type="range" step="1" max="100">
                </div>
                <button class="video-forward">
                  <img src="{{ 'forward.svg' | asset_url }}" alt="forward video" title="forward video" width="12" height="12">
                </button>
                <button class="video-fullscreen">
                  <img src="{{ 'fullscreen.svg' | asset_url }}" alt="full screen" title="full screen" width="18" height="10">
                </button>
                <button class="video-muted">
                  <img src="{{ 'sound_off.svg' | asset_url }}" alt="soundoff video" title="muted video" width="13" height="10">
                  <img src="{{ 'sound_on.svg' | asset_url }}" alt="soundon video" width="13" height="10" aria-hidden="true">
                </button>
              </div>
            </div>
          </div>
        </div>
      {% endif %}
    {% else %}
      {% if section.settings.vimeo_url != blank %}
        <div class="custom_video_wrapper">
          <video class="custom-video" autoplay playsinline muted loop id="video-{{ section.id }}">
            <source type="video/mp4" src="{{ section.settings.vimeo_url }}">
          </video>
        </div>
      {% endif %}
    {% endif %}
  </div>
</section>

<script>
document.addEventListener('DOMContentLoaded', function() {
    var videoContainers = document.querySelectorAll('.video-container');

    videoContainers.forEach(function(container) {
        var video = container.querySelector('video');
        if (!video) return; // 動画要素が取得できなかった場合は処理を終了

        // PC用とスマートフォン用の動画URLをdata属性から取得
        var pcVideoUrl = container.getAttribute('data-pc-video-url');
        var mobileVideoUrl = container.getAttribute('data-mobile-video-url');

        // デバウンス用のタイマー
        var resizeTimeout;

        // デバイスタイプに応じてsrc属性を設定
        function setVideoSource() {
            var currentTime = video.currentTime;
            var isPlaying = !video.paused;

            if (mobileVideoUrl && window.matchMedia && window.matchMedia('(max-width: 1024px)').matches) {
                // スマートフォンの場合でmobileVideoUrlが設定されている場合
                if (video.getAttribute('src') !== mobileVideoUrl) {
                    video.setAttribute('src', mobileVideoUrl);
                }
            } else {
                // それ以外の場合、またはmobileVideoUrlが設定されていない場合
                if (video.getAttribute('src') !== pcVideoUrl) {
                    video.setAttribute('src', pcVideoUrl);
                }
            }

            // 動画が新しいURLで再生されるようにする
            video.addEventListener('loadedmetadata', function() {
                video.currentTime = currentTime;
                if (isPlaying) {
                    video.play();
                }
            }, { once: true });
        }

        // 初回の動画URL設定
        setVideoSource();

        // リサイズイベントをデバウンスする
        window.addEventListener('resize', function() {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(function() {
                setVideoSource();
            }, 250); // 250ms後にsetVideoSourceを実行
        });
    });
});
</script>

{% schema %}
{
  "name": "Custom Video Kv",
  "class": "section-overlay-header",
  "settings": [
    {
      "type": "text",
      "id": "vimeo_url",
      "label": "vimeo URL"
    },
    {
      "type": "text",
      "id": "vimeo_url_mobile",
      "label": "vimeo URL mobile"
    },
    {
      "type": "checkbox",
      "id": "vimeo_controls",
      "label": "view controls"
    }
  ],
  "presets": [
    {
      "name": "Custom Video Kv",
      "category": "Image"
    }
  ]
}
{% endschema %}

fadeIn1sというcssは、プレイヤーのデザインが当たっていないオリジナルのcss表示が、ページの読み込み時にチラッと見えるのを回避するためのものです。
他はvideoのサイズ指定です。

schemaの前にあるjsは、画面サイズでPCとスマホの動画を切り替えるためのスクリプトです。
デバウンズというのは、スマホでスクロールするとリサイズ処理が走るため、自動再生が何回も発火するのを回避するおまじないです。

アイコンのsvgは、いいフリー素材あります。

ICOOON MONO
https://icooon-mono.com/?s=%E3%82%B9%E3%83%94%E3%83%BC%E3%82%AB%E3%83%BC

js(player.js)

document.addEventListener('DOMContentLoaded', function() {
    var videoContainers = document.querySelectorAll('.video-container');

    videoContainers.forEach(function(container) {
        var videoId = container.getAttribute('data-video-id');
        var video = document.getElementById('video-' + videoId);
        var playButton = container.querySelector('.video-play');
        var progressBar = container.querySelector('.video-progressBar');
        var seekBar = container.querySelector('.video-seek');
        var forwardButton = container.querySelector('.video-forward');
        var fullscreenButton = container.querySelector('.video-fullscreen');
        var muteButton = container.querySelector('.video-muted');

        // 初期状態の変数
        var isPlaying = !video.paused;
        var isMuted = video.muted;

        // 初期状態のアイコン設定
        playButton.querySelector('img[alt="play video"]').style.display = 'block';
        playButton.querySelector('img[alt="pause video"]').style.display = 'none';

        // 自動再生後にアイコンを一時停止に変更
        video.addEventListener('play', function() {
            playButton.querySelector('img[alt="play video"]').style.display = 'none';
            playButton.querySelector('img[alt="pause video"]').style.display = 'block';
        });

        video.addEventListener('pause', function() {
            playButton.querySelector('img[alt="play video"]').style.display = 'block';
            playButton.querySelector('img[alt="pause video"]').style.display = 'none';
        });

        // Play/Pause functionality
        playButton.addEventListener('click', function() {
            if (video.paused) {
                video.play();
            } else {
                video.pause();
            }
        });

        // Update progress bar and seek bar
        video.addEventListener('timeupdate', function() {
            if (isFinite(video.duration) && isFinite(video.currentTime)) {
                var value = (video.currentTime / video.duration) * 100;
                progressBar.value = value;
                seekBar.value = video.currentTime;
            }
        });

        // Set seek bar max value once metadata is loaded
        video.addEventListener('loadedmetadata', function() {
            if (isFinite(video.duration)) {
                seekBar.max = video.duration;
                progressBar.max = 100;
            }
        });

        // Seek functionality
        seekBar.addEventListener('input', function() {
            if (isFinite(video.duration)) {
                video.currentTime = seekBar.value;
            }
        });

        // Forward functionality
        forwardButton.addEventListener('click', function() {
            video.currentTime += 10;
        });

        // Fullscreen functionality
        fullscreenButton.addEventListener('click', function() {
            if (video.requestFullscreen) {
                if (document.fullscreenElement) {
                    document.exitFullscreen();
                } else {
                    video.requestFullscreen();
                }
            } else if (video.webkitEnterFullScreen) { // iOS Safari
                video.webkitEnterFullScreen();
            } else if (video.mozRequestFullScreen) {
                if (document.mozFullScreenElement) {
                    document.mozCancelFullScreen();
                } else {
                    video.mozRequestFullScreen();
                }
            } else if (video.msRequestFullscreen) {
                if (document.msFullscreenElement) {
                    document.msExitFullscreen();
                } else {
                    video.msRequestFullscreen();
                }
            }
        });

        // Mute/Unmute functionality
        muteButton.addEventListener('click', function() {
            video.muted = !video.muted;
            isMuted = video.muted;

            if (isMuted) {
                muteButton.querySelector('img[alt="soundoff video"]').style.display = 'block';
                muteButton.querySelector('img[alt="soundon video"]').style.display = 'none';
            } else {
                muteButton.querySelector('img[alt="soundoff video"]').style.display = 'none';
                muteButton.querySelector('img[alt="soundon video"]').style.display = 'block';
            }
        });
    });
});

プレイヤーを動作させるスクリプトですが、今回。
5行〜6行目の記述で、動画とプレイヤーを一致させるためのidを仕込みました。
shopifyではカスタマイズでブロックを増やすと、sectionに自動でidが割り振られるようで、{{ section.id }}というそのidを利用しました。

自動でのidがない時は、面倒ですが専用にスキーマを作ってカスタマイズでidを自由に登録しても良いのではないでしょうか。

・css(player.css)

.video-container {
  width: 100%;
  height: 100%;
  position: relative;
}

.custom_video_inner {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.video-controls {
  position: absolute;
  bottom: 2rem;
  width: 85%;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-pack: distribute;
  justify-content: space-around;
  left: 50%;
  transform: translateX(-50%);
}

@media (min-width: 64em) {
  .video-controls {
    width: 68%;
  }
}

.video-controls button {
  background: transparent;
  color: #fff;
  border: none;
  cursor: pointer;
  padding: 0;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  outline: none;
}

.video-controls img {
  width: 20px;
}

.video-play {
  width: 1.8rem;
  margin-right: 0.8rem;
}

.video-play:not(.-pause) img:nth-child(1) {
  display: none;
}

.video-play.-pause img:nth-child(2) {
  display: none;
}

.video-muted:not(.-soundon) img:nth-child(2) {
  display: none;
}

.video-muted.-soundon img:nth-child(1) {
  display: none;
}


.video-fullscreen,
.video-forward {
  width: 1.8rem;
  margin-left: 1.0rem;
}

.video-forward {
  display: none !important;
}

.video-muted {
  margin-left: 0.4rem;
}

.video-muted img {
  width: 20px;
}

.video-timeline {
  -ms-flex: 1;
  flex: 1;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  border: none;
  position: relative;
}

.video-timeline input {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  width: 100%;
  height: 2px; /* スライダーの高さをバーと一致させる */
  background: transparent;
  cursor: pointer;
  position: absolute;
}

.video-timeline input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 0; /* つまみの幅を0に設定 */
  height: 0; /* つまみの高さを0に設定 */
  background: transparent; /* 背景を透明にする */
}

.video-timeline input::-moz-range-thumb {
  -moz-appearance: none;
  appearance: none;
  width: 0; /* つまみの幅を0に設定 */
  height: 0; /* つまみの高さを0に設定 */
  background: transparent; /* 背景を透明にする */
}

.video-timeline input::-ms-thumb {
  -ms-appearance: none;
  appearance: none;
  width: 0; /* つまみの幅を0に設定 */
  height: 0; /* つまみの高さを0に設定 */
  background: transparent; /* 背景を透明にする */
}

.video-progressBar {
  background: #a4a4a4;
  background-color: #a4a4a4;
  height: 2px;
  -ms-flex: 1;
  flex: 1;
  appearance: none;
  position: absolute;
  width: 100%;
  border: none;
}

.video-progressBar::-webkit-progress-bar {
  background-color: #a4a4a4; /* 背景色 */
}

.video-progressBar::-webkit-progress-value {
  background: #fff; /* 進行部分の色 */
  background-color: #fff;
}

.video-progressBar::-moz-progress-bar {
  background: #a4a4a4; /* 背景色 */
}

.video-timeline input::-moz-range-progress {
  background: #fff; /* 進行部分の色 */
}

.video-progressBar::-ms-fill {
  background: #fff; /* 進行部分の色 */
}

.video-container:hover .video-controls {
  opacity: 1;
}

:fullscreen {
  /* フルスクリーン時のスタイル */
  width: 100%;
  height: 100%;
}

:-webkit-full-screen {
  /* Safari用のフルスクリーンスタイル */
  width: 100%;
  height: 100%;
}

input.video-seek {
  margin: 0;
  padding: 0;
  background: none;
  border: none;
  border-radius: 0;
  outline: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

プレイヤーの部分のデザイン表示のためのcssたちです。

以上、メモになります。

いろいろな方法があると思います。
今回私はライブラリのjs改修はうまくいかず、諦めてプレーンな状態からスタートさせました。

参考資料置き場

電脳情報局
https://www.omakase.net/blog/2021/12/videojs.html

vimeoダウンロード
https://vimeo.com/jp/features/video-editor/download-video

ICOOON MONO
https://icooon-mono.com/?s=%E3%82%B9%E3%83%94%E3%83%BC%E3%82%AB%E3%83%BC