Vega OS 向け Kollus プレイヤー組み込みガイド

Vega OS 向け Kollus プレイヤー組み込みガイド

1. 全体方針とアーキテクチャ

1-1. 採用構成

Vega OS 向け Kollus プレイヤーは、以下の構成で実装します。

  • React Native + WebView + iframe 構成

  • アプリ本体: React Native

  • 動画再生ビュー: WebViewコンポーネント(@amazon-devices/webview)

  • プレイヤー本体: WebView 内の Kollus HTML5 プレイヤー

  • Kollus プレイヤーとの連携: WebView 内の iframe + VG-Controller JavaScript

1-2. WebView方式を標準とする理由

Vega OS 向けのアプリ開発では、WebView を中心に UI と機能を構築する開発スタイルが公式に案内されています。
このため、Kollus HTML5 プレイヤーを WebView 経由で利用する方式を標準構成とします。

また、Android で一般的な ExoPlayer / Media3 などのネイティブ動画再生基盤を前提とした実装は、Vega OS の想定アーキテクチャ(React Native + WebView)と整合しないため、採用しません。

 

この方針により、以下を狙います。

  • Vega OS の公式ガイドライン/実装パターンに沿った構成での安定運用

  • プレイヤー改修を HTML 側で完結させやすく、アップデート反映が迅速になる

  • ネイティブ実装に依存しないことで、端末差分・OS差分の影響を局所化できる


2. 技術的な注意点(CORS / ドメイン構成)

2-1. Vega OS WebView / Silk Browser の挙動

  • Vega OS の WebViewコンポーネント および Silk Browser では、

    • 親ドメインとは異なるドメインから Widevine 動画を再生する場合、

    • CORS 問題(推定)により無限バッファリングが発生する事象を確認しています。

    • Fire OSのSilk Browserでは正常再生を確認しました。

  • 本件について Amazon 側と確認を進めており、
    Amazon側で修正パッチの適用が予定されています。(2025/12/7更新)

2-2. サンプルページのドメイン構成

サンプルページ:
https://web.as1as.net/jp/vega_test2.php

構成は次の通りです。

  1. web.as1as.net/jp/vega_test2.php

    • ページ本体。

    • ページ内 <iframe> から、同一ドメインの
      https://web.as1as.net/player?... をトークン付きで呼び出し。

  2. web.as1as.net/player

    • nginx のリバースプロキシ経由で
      https://v.jp.kollus.com へアクセスし、パラメータ(JWT など)を引き継ぐ。

  3. v.jp.kollus.com

    • Kollus のプレイヤー配信ドメイン。

ポイント:

  • WebView 側からは常に web.as1as.net を叩くようにし、

  • その背後で nginx が v.jp.kollus.com へプロキシすることで、

  • CORS の影響を最小化する構成としています。


3. 開発環境

3-1. Vega OS アプリ開発環境

3-2. リモコン連携


4. サーバー側設定(nginx リバースプロキシ)

Vega OS WebView からは、Kollus プレイヤーを直接 v.jp.kollus.com で呼び出さず、
フロントドメイン(例: web.as1as.net)配下でプロキシする構成を推奨します。

4-1. サンプル nginx 設定

location /player { proxy_pass https://v.jp.kollus_com/s; proxy_set_header Host v.jp.kollus.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # CORS ヘッダー(必要に応じて調整) add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; # JWT が大きくなる場合のバッファ調整 proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; proxy_ssl_verify off; proxy_ssl_server_name on; } location ~ ^/(js|css|fonts|images|assets)/ { proxy_pass https://v.jp.kollus_com; proxy_set_header Host v.jp.kollus.com; proxy_ssl_server_name on; proxy_ssl_name v.jp.kollus.com; proxy_ssl_verify off; proxy_cache_valid 200 1d; expires 1d; add_header Cache-Control "public, immutable"; }
  • /player : Vega OS WebView からアクセスするプレイヤーエンドポイント

  • /js, /css, /fonts, /images, /assets : 静的アセットのキャッシュ設定


5. Vega OS アプリ側サンプルコード

Vega OS 向けの React Native + WebView サンプルは、GitHub で公開しています。

このサンプルでは、React Native アプリ内の WebView から
https://web.as1as.net/jp/vega_test2.php を表示し、
その中の iframe によって Kollus プレイヤーをロードする構成になっています。


6. Web ページ側サンプルコード(iframe + VG-Controller)

以下は、実際に Vega OS 上の WebView から呼び出す HTML サンプルです。
(実体: https://web.as1as.net/jp/vega_test2.php)

6-1. 役割の概要

  • <iframe id="iframe">

    • https://web.as1as.net/player?jwt={JWT}&custom_key={CUSTOM_KEY}&autoplay&controls_activation=none

    • ここで Kollus プレイヤーが実行されます。

  • 外側の JS (vg-controller-client)

    • iframe 内のプレイヤーを制御するため、

      • 再生 / 一時停止、10秒スキップ、区間リピート、倍速、音量、全画面 などを提供。

      • コントロールバーをカスタムスキンに変更可能、サンプルコード含め。

6-2. サンプルコード(抜粋)

<html> <head> <meta charset="utf-8"/> <style> /* 画面全体をプレイヤーに割り当て */ html, body { height: 100%; margin: 0; padding: 0; } /* プレイヤーコンテナ(iframe + コントロール) */ #player-container { width: 100%; height: 100%; margin: 0; position: relative; background: #fff; display: flex; flex-direction: column; } /* Kollus プレイヤーを表示する iframe */ #iframe { width: 100%; height: 100%; border: none; flex: 1; } /* 下部に重ねるコントロールバー */ #controls-wrapper { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); width: 95%; max-width: 800px; background: rgba(255, 255, 255, 0.85); padding: 10px; border-radius: 8px; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; } @media (max-width: 800px) { #controls-wrapper { width: 90%; font-size: 0.8rem; } #controls button, #playback-controls button, #controls select { font-size: 0.8rem; padding: 4px 6px; } } </style> <!-- Kollus VG-Controller ライブラリ --> <script src="https://file.kollus.com/vgcontroller/vg-controller-client.latest.min.js"></script> <!-- 任意: UI 用ライブラリ(例: Bootstrap) --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script> var controller; // Kollus.VideogatewayController インスタンス var status; window.onload = function() { // iframe 内プレイヤーを制御するコントローラ初期化 controller = new Kollus.VideogatewayController({ target_window: document.getElementById('iframe').contentWindow }); // 既定のプレイヤーコントロールを非表示(カスタム UI を使用) controller.set_controls_activity(false); controller.set_control_visibility(false); controller.set_controlbar_progress_only(true); // 再生状態イベント controller.on('play', function() { status = 'play'; }) .on('pause', function() { status = 'pause'; }); // カスタムボタンのイベントを設定 setupButtonControls(); }; function setupButtonControls() { // ---- 再生 / 一時停止 ---- var isPlaying = false; var playPauseBtn = document.getElementById('playPauseBtn'); playPauseBtn.addEventListener('click', function() { if (isPlaying) { controller.pause(); playPauseBtn.textContent = '▶️ play'; } else { controller.play(); playPauseBtn.textContent = '⏸️ pause'; } isPlaying = !isPlaying; }); controller.on('play', function() { isPlaying = true; playPauseBtn.textContent = '⏸️ pause'; }); controller.on('pause', function() { isPlaying = false; playPauseBtn.textContent = '▶️ play'; }); // ---- 10秒戻し / 10秒送り ---- document.getElementById('rwBtn').addEventListener('click', function() { controller.rw(10); }); document.getElementById('ffBtn').addEventListener('click', function() { controller.ff(10); }); // ---- 区間リピート ---- var loopStart = null; var loopEnd = null; var looping = false; document.getElementById('loopStartBtn').addEventListener('click', function() { loopStart = controller.get_current_time(); this.classList.remove('btn-outline-secondary'); this.classList.add('btn-success'); }); document.getElementById('loopEndBtn').addEventListener('click', function() { loopEnd = controller.get_current_time(); looping = true; this.classList.remove('btn-outline-secondary'); this.classList.add('btn-success'); }); document.getElementById('loopClearBtn').addEventListener('click', function() { loopStart = null; loopEnd = null; looping = false; document.getElementById('loopStartBtn').classList.remove('btn-success'); document.getElementById('loopStartBtn').classList.add('btn-outline-secondary'); document.getElementById('loopEndBtn').classList.remove('btn-success'); document.getElementById('loopEndBtn').classList.add('btn-outline-secondary'); }); controller.on('progress', function(percent, position, duration) { if (looping && loopStart !== null && loopEnd !== null && position >= loopEnd) { controller.set_current_time(loopStart); controller.play(loopStart); } document.getElementById('timeDisplay').textContent = formatTime(position) + " / " + formatTime(duration); }); function formatTime(sec) { var m = Math.floor(sec / 60); var s = Math.floor(sec % 60); return (m < 10 ? '0' + m : m) + ":" + (s < 10 ? '0' + s : s); } // ---- 倍速 ---- document.getElementById('speedSelect').addEventListener('change', function() { controller.set_speed(parseFloat(this.value)); }); // ---- 音量 / ミュート ---- var volumeSlider = document.getElementById('volumeSlider'); var muteBtn = document.getElementById('muteBtn'); var lastVolume = volumeSlider.value; muteBtn.addEventListener('click', function() { var vol = Number(volumeSlider.value); if (vol === 0 && lastVolume > 0) { controller.set_volume(lastVolume); volumeSlider.value = lastVolume; muteBtn.textContent = '🔈'; } else { lastVolume = vol; controller.set_volume(0); volumeSlider.value = 0; muteBtn.textContent = '🔇'; } }); volumeSlider.addEventListener('input', function() { var vol = Number(volumeSlider.value); controller.set_volume(vol); if (vol === 0) { muteBtn.textContent = '🔇'; } else { muteBtn.textContent = '🔈'; lastVolume = vol; } }); controller.on('volumechange', function(volume) { volumeSlider.value = volume; muteBtn.textContent = volume === 0 ? '🔇' : '🔈'; }); // ---- 全画面 ---- document.getElementById('fullscreenBtn').addEventListener('click', function() { var container = document.getElementById('player-container'); if (!document.fullscreenElement) { if (container.requestFullscreen) container.requestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); } }); } </script> </head> <body> <div id="player-container"> <!-- Kollus プレイヤー iframe --> <iframe id="iframe" width="100%" height="100%" src="https://web.as1as.net/player?jwt={JWT}&custom_key={CUSTOM_KEY}&autoplay&controls_activation=none" allowfullscreen webkitallowfullscreen mozallowfullscreen allow="encrypted-media;autoplay"> </iframe> <!-- カスタムコントロール --> <div id="controls-wrapper"> <div id="playback-controls"> <button id="playPauseBtn">▶️ play</button> <button id="rwBtn">⏪ Rewind 10s</button> <button id="ffBtn">⏩ Forward 10s</button> <button id="loopStartBtn">Start Loop</button> <button id="loopEndBtn">End Loop</button> <button id="loopClearBtn">Clear Loop</button> </div> <div id="controls"> <label for="speedSelect">Speed:</label> <select id="speedSelect"> <option value="0.5">0.5x</option> ... <option value="2">2x</option> </select> <button id="muteBtn">🔈</button> <input type="range" id="volumeSlider" min="0" max="100" value="70"> <span id="timeDisplay">00:00 / 00:00</span> <button id="fullscreenBtn">Fullscreen</button> </div> </div> </div> </body> </html>

{JWT}, {CUSTOM_KEY} 部分は、実際のトークンやカスタムキーに置き換えてください。

Copyright © CATENOID, lnc. All Rights Reserved.
E-mail. jp_sales@catenoid.net | Tel. 03-4405-8462