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
構成は次の通りです。
web.as1as.net/jp/vega_test2.phpページ本体。
ページ内
<iframe>から、同一ドメインのhttps://web.as1as.net/player?...をトークン付きで呼び出し。
web.as1as.net/playernginx のリバースプロキシ経由で
https://v.jp.kollus.comへアクセスし、パラメータ(JWT など)を引き継ぐ。
v.jp.kollus.comKollus のプレイヤー配信ドメイン。
ポイント:
WebView 側からは常に
web.as1as.netを叩くようにし、その背後で nginx が
v.jp.kollus.comへプロキシすることで、CORS の影響を最小化する構成としています。
3. 開発環境
3-1. Vega OS アプリ開発環境
開発マシンはVega仮想デバイスを使う場合、MシリーズMacが前提(サポート対象)です。
Vega OS アプリ開発全体の流れは Amazon の公式ドキュメントを参照してください。
https://developer.amazon.com/docs/vega/0.21/build-an-app.html
https://developer.amazon.com/docs/vega/0.21/overview-of-webview.html
3-2. リモコン連携
Vega OS アプリから WebView 上の HTML5 プレイヤーに対するリモコン連携は、
Amazon のガイドに記載されている 対応コードのボタンのみ利用できます。
詳細仕様は以下を参照してください。
https://developer.amazon.com/docs/vega/0.21/develop-your-app-with-webview.html
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 で公開しています。
インストール・ビルド・実行方法は、リポジトリ内の
README.mdを参照してください。
このサンプルでは、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