スカパー!を宅内配信&リモコン操作する
この夏休みに久しぶりモノづくりしたので紹介します。
概要
自分の部屋から離れたリビングにある、スカパー!チューナーの映像をWindows機に表示し、さらにリモコンでコントロールできるようにしました。画面はこんな感じです。
主に、番組予約や、録画した番組をネットワークHDDにコピーするために使用しています。
方針
元々は、安さに惹かれてコステルのWiTV
を使っていたのですが、ルーターやハブを差し替えたらPCから使えなくなってしまいました(WiTVをリセットしたら、Wi-Fi接続のAndroid機からは再度使えるようになったんですけどね・・・)。そこで、なるべくお金をかけずに同じことができるようなシステムを構築してみました。
ハード構成
なるべく手持ちの機材を生かしつつ、最小限の投資で実現を目指します。Windows10も入らない、手持ちのPentium M搭載のノートPC
にWindows7を入れて常時ONでサーバーに。すでにキーボードのいくつかは文字が入りませんが、どうせリモートデスクトップで使うことになるので問題ありません。HDDはSSDに換装してあり、比較的まともに動作しています。きっと、天寿をまっとうしてくれることでしょう。
そして、手持ちのコンポジットビデオ/SビデオのUSBキャプチャユニット
を使い、スカパー!チューナーの画像と音声をサーバーに流し込みます。
さらに、スカパー!チューナーをPCからリモートコントーロールするために、ビットトレードワンのUSB接続 赤外線リモコンキット
を購入しました。およそ二千円也。購入品はこれ1つです。
システム全体の接続は下図のようになります。
ソフトウェア要素技術
どのような技術を使ってシステム構築するか考えていきます。
表示・入力を請け負うクライアント・ソフトウェア
ビデオ映像をWebブラウザに表示し、このページ上でリモコン・コントロールします。
映像のネット送信
サーバー、クライアントともに、VLCメディアプレーヤーを使います。VLCメディアプレーヤーには、キャプチャデバイスの映像をストリーミングで送受信する機能があり、またビデオを表示するためのWebブラウザ向けのプラグインも用意されています。今回は早く動かすところまで持っていきたいので、クライアントのWebブラウザとしてInternet Explorerだけをターゲットに開発していきます。また、クライアントPCにインストールするVLCメディアプレーヤーは32bit版を使用します。私は64bit版をインストールした環境でどうしても画像が表示されず、かなり悩みました。
リモコンコントロール
USB接続 赤外線リモコンキット向けに、リモコン送信するコマンドが公開されています(素晴らしい!)。作成者に感謝しつつ、そのまま利用させていただきます。
サーバープログラム
Node.jsを利用し、LAN内だけの使用なので、安直にセキュリティなど考慮せずに必要最低限の機能を実装します。上記リモコンをコントロールするコマンドをNode.jsから呼び出して、USB接続 赤外線リモコンキットをコントロールします。そして、LAN内の自室PCに向けてWebサーバー機能を提供します。
ソフト構成
全体構成
上記の要素技術を組み合わせてシステム構築します。ソフトウェアの全体構成は、下図のようになります。
VLCメディアプレーヤーによる画像配信
最初に、サーバーPCとクライアントPCにVLCメディアプレーヤーをインストールして、ビデオ画像の配信と受信が問題なくできるか確認しておきます。
配信側
VLCメディアプレーヤーを起動し、メニューから[メディア]-[ストリーム...]を選択すると「メディアを開く」ダイアログが開くので、「キャプチャデバイス」タブを開きます。
ここから、ウィザード形式で配信方法を入力していきます。[ストリーム再生]ボタンを押下すると「ストリーム出力」ダイアログの入力元画面
が開くので、[次へ]ボタンを押下。出力先の設定が開く
ので、出力先にHTTPを選択し、[追加]ボタンを押下。新たにHTTPタブが開く
ので、[次へ]ボタンを押下。トランスコーディングオプションの画面
で「トランスコーディングを有効にする」チェックボックスを外し、[次へ]ボタンを押下。ここで、トランスコーディングを無効にするのは、マシンが非力なためです。 オプション設定の画面が開きます。
ここに表示される「生成されたストリーム出力文字列」は後で使いますので、テキストファイル等に保存しておきましょう。[ストリーム]ボタンを押下すれば、ストリーミングの配信が始まります。もし、ファイアウォールがブロックしている旨のダイアログボックスが出たら、アクセスを許可します。
これまでの手順は長くて、試行錯誤や通常運用するには向いていません。そこで先ほど保存した「生成されたストリーム出力文字列」が役に立ちます。VLCメディアプレーヤーはコマンドライン引数に入力元とストリーム出力文字列を指定すれば、ストリーミング配信してくれます。例えば、下記のようなバッチファイルを作っておけば、ダブルクリックするだけでストリーミング配信できます。
クライアント側
映像がきちんと配信できているか、クライアント側で確認します。VLCメディアプレーヤーを起動し、メニューから[メディア]-[ネットワークストリームを開く...]を選択すると「メディアを開く」ダイアログが開くので、「ネットワーク」タブを開き、配信側PCのURLを入力します。例えば、IPアドレスが「192.168.0.254」だったら下図のようになります。
そして、[再生]ボタンを押して映像が出てくれば、配信の確認は完了です。
HTML
画面のデザインを担うHTMLファイルの内容を解説します。
ビデオ画像の埋め込み
さきのネットワークストリームの配信の確認ではクライアント側にVLCメディアプレーヤーを使いましたが、本番運用ではVLC WebPluginを使います。WebPluginのドキュメントにはHTMLファイルにどう記述するか書いてあるので、これを参考にWebPluginで画像が表示できるか確認します。クライアントPC側でテスト用のHTMLファイルを開き、画像が表示できればOKです。例えば、下記のようなHTMLファイルを開いてみます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>VLC WebPlugin TEST</title>
</head>
<body>
<object type="application/x-vlc-plugin" id="vlc" width="854" height="480" classid="clsid:9BE31822-FDAD-461B-AD51-BE1D1C159921" windowless="true" codebase="http://download.videolan.org/pub/videolan/vlc/last/win32/axvlc.cab">
<param name="target" value="http://192.168.0.254:8080" />
</object>
</body>
</html>
アスペクト比が正しくない以外は問題なさそうです。WebPluginのドキュメントによれば、アスペクト比を変えるにはVideo Objectにアクセスする必要があるようなので、後ほどJavaScriptで対応したいと思います。
リモコンの操作子をSVGで作成する
リモコンの操作子は、HTMLとの親和性が高そうなのでSVGを使って描画します。描画ツールはInkscapeを使い、実際のスカパー!チューナーのリモコンを参考にデザインしました。
シンプルさを優先し、ボタン操作への応答もSVG内に書いていきます。これも、Inkscapeを使って記述することができるので、ボタンとイベントハンドラの関係が視覚的にわかりやすくて良いですね。まず、操作への応答を記述したいオブジェクトを選択し、「オブジェクトの プロパティ」を開きます。「インタラクティビティ」の項目を展開すると、各イベントに対する応答を記述できるようになります。
ボタン操作への応答は、jQueryでAjaxを使います。例えば、電源ボタンの操作への応答は、以下のように記述します。
$.ajax({url: "BtnPower", cache: false});
どのボタンを押したか識別するためにurlに自分でつけたボタン名を指定します。また、Internet ExplorerでAjaxを使う場合、一度使ったurlのデータはキャッシュの内容を使用するため、再度同じボタンを押したときサーバーがボタンの押下を検出できません。そこで、cacheをfalseとしてキャッシュを無効にします。
インタラクティビティの指定は、オブジェクトごとに指定します。そのため、ボタンの上に文字やシンボルが描画されている場合は、押した場所の微妙な違いによりイベントを受け取るオブジェクトが異なってしまいます。そこで、ボタン上のオブジェクトはすべてボタン枠のオブジェクトと一緒にグループ化して、グループに対してインタラクティビティを設定します。グループ化したいオブジェクトを選択し、メニューから[オブジェクト]-[グループ化]を選択するか、ツールバーの「選択したオブジェクトをグループ化」ボタンを押せばグループ化できます。
このような手順で、すべてのボタンのインタラクティビティを設定します。
SVGをHTMLに埋め込む
リモコン操作子への応答をSVGに埋め込んだので、HTMLにSVGを埋め込む必要があります。テキストエディタでカットアンドペーストしてしまうと保守性が悪いので、SVGをJavaScriptでHTMLのBODYに読み込みます。まず、HTMLに埋め込み先を作成
<body>
:
<p id="remocon">読み込み中</p>
:
</body>
します。このpエレメントにSVGが読み込まれると、「読み込み中」の文字がリモコン操作子に置き換わります。SVGの読み込みはHTMLの読み込みが終わったところで、下記のように読み込みます。
$(function() {
$.get("remocon.svg", function (data) {
$("#remocon").html(data);
});
JavaScriptのコードが出てきたので、ついでに先ほどおかしかったアスペクト比を16:9に固定します。
var vlc = document.getElementById("vlc");
vlc.video.aspectRatio = "16:9";
});
HTML上の文字列の扱いを変更する
リモコンの操作子に置いた文字は、HTML上で文字として扱われるので、選択可能ですし、文字上ではマウスカーソルもI型になってしまいます。そこで下記のように、スタイルシートでマウスカーソルを常に矢印に変更し、文字列は選択できないようにします。
body {
cursor:default;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
実装
最終的に、HTML(irRemote.html)のソースは下記のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
<!--
body {
cursor:default;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
p#remocon {margin-top:0px; margin-bottom:0px;}
-->
</style>
<script type="text/javascript" src="./jquery-3.1.0.min.js"></script>
<script type="text/javascript" src="./irRemote.js"></script>
<title>スカパーリモコン</title>
</head>
<body>
<div id="tv">
<object type="application/x-vlc-plugin" id="vlc" width="854" height="480" classid="clsid:9BE31822-FDAD-461B-AD51-BE1D1C159921" windowless="true" codebase="http://download.videolan.org/pub/videolan/vlc/last/win32/axvlc.cab">
<param name="target" value="http://192.168.0.254:8080" />
<param name="windowless" value="true" />
</object>
</div>
<p id="remocon">読み込み中</p>
</body>
</html>
そして、このHTMLが読み込むJavaScript(irRemote.js)は下記のようになります。
$(function() {
$.get("remocon.svg", function (data) {
$("#remocon").html(data);
});
var vlc = document.getElementById("vlc");
vlc.video.aspectRatio = "16:9";
});
クライアントPC上で動作するコードは以上です。次に、サーバー側のPCで動作するコードを書いていきます。
サーバープログラム
サーバープログラムはNode.js上に構築します。基本的にサーバープログラムはクライアントPCが要求するデータを返し、リモコンの操作子に割り当てた名前がurlに渡されてくれば、赤外線リモコンキットにリモコンデータを送ります。
クライアントPCのWebブラウザにデータを渡す
クライアントPCのWebブラウザでサーバーにアクセスすると、最初にHTMLのデータが要求され、芋づる式にJavaScriptのコードなどが要求されていきます。簡単なサーバープログラムを作って、どのようなデータが要求されるのか観察してみましょう。以下のコード(ServerTest.js)は、ポート8888で待ち受けしつつ、クライアントのWebブラウザが要求するurlをコマンドプロンプトに表示します。
var http = require('http');
var url = require('url');
var server = http.createServer();
server.on('request', function (req, res) {
var uri = url.parse(req.url, true);
console.log(uri.pathname);
res.end();
});
server.listen(8888);
console.log('Server running!');
これを、
node ServerTest.js
のようにして起動し、クライアントPCのInternet Explorerでurlに
http://192.168.0.254:8888/
のように指定すると。
/
/favicon.ico
とサーバーのコマンドプロンプトに表示されます。ルートのデータとアイコンファイルを要求しているので、先ほど作ったHTML(irRemote.html)のデータをWebブラウザに返してやることにします。favicon.icoファイルは適当な内容で用意しておきます。
var http = require('http');
var fs = require('fs');
var url = require('url');
var server = http.createServer();
server.on('request', function (req, res) {
var uri = url.parse(req.url, true);
if (uri.pathname === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
fs.createReadStream("./irRemote.html").pipe(res);
} else if (uri.pathname === "/favicon.ico") {
fs.createReadStream("./favicon.ico").pipe(res);
} else {
console.log(uri.pathname);
res.end();
}
});
server.listen(8888);
console.log('Server running!');
これをサーバー側で実行し、クライアント側Webブラウザで、**http://192.168.0.254:8888/**にアクセスします。 以下が、サーバー側の実行結果です。
C:\Users\tohshima\Documents>node ServerTest.js
Server running!
/jquery-3.1.0.min.js
/irRemote.js
jquery-3.1.0.min.jsとirRemote.jsを返さなければいけないことがわかります。これらを返せるようにサーバープログラムのソースを改造します。
var http = require('http');
var fs = require('fs');
var url = require('url');
var server = http.createServer();
server.on('request', function (req, res) {
var uri = url.parse(req.url, true);
if (uri.pathname === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
fs.createReadStream("./irRemote.html").pipe(res);
} else if (uri.pathname === "/favicon.ico") {
fs.createReadStream("./favicon.ico").pipe(res);
} else if (uri.pathname === "/jquery-3.1.0.min.js") {
fs.createReadStream("./jquery-3.1.0.min.js").pipe(res);
} else if (uri.pathname === "/irRemote.js") {
fs.createReadStream("./irRemote.js").pipe(res);
} else {
console.log(uri.pathname);
res.end();
}
});
server.listen(8888);
console.log('Server running!');
サーバープログラムを実行すると更にレスポンスが必要なようです。
C:\Users\tohshima\Documents>node ServerTest.js
Server running!
/remocon.svg
さらにremocon.svgを返すようにサーバープログラムのソースを改造すると、
C:\Users\tohshima\Documents>node ServerTest.js
Server running!
追加のデータ要求はなく、とりあえず表示は良さそうです。
次にリモコンのボタンを押してみます。[メニュー]ボタンを押すとurlとして、ボタンに付けた名前が返されます。
C:\Users\tohshima\Documents>node ServerTest.js
Server running!
/BtnMenu
これまでやってきた様に、ボタン名で処理内容を分岐させれば良さそうですが、ボタン数が多いので少し工夫します・・・が、その前にリモコンの操作用にリモコンに送るデータを作成します。
リモコン操作用データを作る
最初に説明したように、USB接続 赤外線リモコンキット向けに、リモコン送信するコマンドを作成されてた方がいらっしゃるので、これを利用させていただきます。引数無しでコマンド(BtoIrRemocon.exe)を実行すると、リモコンの待ち受け状態になるので、リモコンのボタンを押しリモコンコードを取得します。ここで得られたリモコンコードを引数にコマンド(BtoIrRemocon.exe)を実行すると、リモコン操作が可能になります。念のためここで、コマンドでリモコン操作ができるか確認しておきます。
使用したいリモコンコードをすべて取得し、リモコンのボタン名と対にしたJavaScriptの連想配列に格納します。私の環境で作成した連想配列は下記のようになります。
exports.btn2ircode = {
"BtnPower" : "C1A22A80173DAA",
"Btn1" : "C1A22A80171087",
"Btn2" : "C1A22A80171186",
"Btn3" : "C1A22A80171285",
"Btn4" : "C1A22A80171384",
"Btn5" : "C1A22A80171483",
"Btn6" : "C1A22A80171582",
"Btn7" : "C1A22A80171681",
"Btn8" : "C1A22A80171780",
"Btn9" : "C1A22A8017188F",
"Btn0" : "C1A22A8017198E",
"BtnStar" : "C1A22A80171A8D",
"BtnSharp" : "C1A22A80171B8C",
"BtnChUp" : "C1A22A801722B5",
"BtnChDown" : "C1A22A801723B4",
"BtnShort" : "C1A22A801721B6",
"BtnRadio" : "C1A22A80178611",
"BtnOSD" : "C1A22A80178710",
"BtnMenu" : "C1A22A801764F3",
"BtnDiscript" : "C1A22A801763F4",
"BtnPrefer" : "C1A22A801720B7",
"BtnRecStart" : "C1A22A801776E1",
"BtnRecList" : "C1A22A80177DEA",
"BtnPlayList" : "C1A22A80178413",
"BtnPrgList" : "C1A22A801761F6",
"BtnUseful" : "C1A22A80179304",
"BtnUp" : "C1A22A801751C6",
"BtnDown" : "C1A22A801752C5",
"BtnRight" : "C1A22A801754C3",
"BtnLeft" : "C1A22A801753C4",
"BtnEnter" : "C1A22A801755C2",
"BtnReturn" : "C1A22A80177CEB",
"BtnSubMenu" : "C1A22A801750C7",
"BtnBlue" : "C1A22A801757C0",
"BtnRed" : "C1A22A801758CF",
"BtnGreen" : "C1A22A801759CE",
"BtnYellow" : "C1A22A80175ACD",
"BtnBiling" : "C1A22A801760F7",
"BtnSubtitle" : "C1A22A801724B3",
"BtnPause" : "C1A22A801765F2",
"Btn30s" : "C1A22A801766F1",
"BtnPlayStart" : "C1A22A801773E4",
"BtnStop" : "C1A22A801770E7",
"BtnRewind" : "C1A22A801771E6",
"BtnForward" : "C1A22A801772E5",
"BtnBefor" : "C1A22A801775E2",
"BtnNext" : "C1A22A801774E3"
};
これをir_id.jsというファイル名で保存します。
リモコン操作子の操作に応答する
リモコン操作子の操作に応答できるように、サーバープログラムを改造していきます。
ここまでくれば、もうurlに応答する部分にリモコンコードを引数にコマンド(BtoIrRemocon.exe)を実行するだけだとわかりますね。最終的なサーバープログラム(irRcServer.js)のソースは下記のようになります。
var http = require('http');
var fs = require('fs');
var url = require('url');
var execFile = require('child_process').execFile;
var irid = require('./ir_id.js');
var server = http.createServer();
server.on('request', function (req, res) {
var uri = url.parse(req.url, true),
btnId;
if (uri.pathname === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
fs.createReadStream("./irRemote.html").pipe(res);
} else if (uri.pathname === "/jquery-3.1.0.min.js") {
fs.createReadStream("./jquery-3.1.0.min.js").pipe(res);
} else if (uri.pathname === "/irRemote.js") {
fs.createReadStream("./irRemote.js").pipe(res);
} else if (uri.pathname === "/remocon.svg") {
fs.createReadStream("./remocon.svg").pipe(res);
} else if (uri.pathname === "/favicon.ico") {
fs.createReadStream("./favicon.ico").pipe(res);
} else {
btnId = irid.btn2ircode[uri.pathname.substr(1)];
if(btnId === undefined) {
console.log("==etc==");
console.log(uri.pathname);
} else {
console.log(uri.pathname.substr(1) + " : " + btnId);
execFile("BtoIrRemocon.exe", [btnId]);
}
res.end();
}
});
server.listen(8888);
console.log('Server running!');
リモコンの操作時にはWebブラウザに何も返さないので、**res.end();**を付けておきます。
以上で完成です。
ハマりどころ
既出、未出含め、ハマりどころをまとめておきます。
VLCは64bit版を入れてはいけない
私の環境では動きませんでした。Internet Explorerはプラグインをロードしているようでしたが、音しか出ず、映像が出ませんでした。
IEのajaxでは、cashをfalseにしないと1度しか発火できない
cashをfalseにしないと、同じボタンが2度と使えない(リロードすればよいのですが)という目にあいます。Internet Explorerだけではないのかな?
USBのセレクティブサスペンドを無効に
「どうも、調子が悪い」と思って色々調べましたが、省電力設定も原因になっていたようです。
作ってみた感想
それほど時間もかけずに動作することろまで持っていけたので満足しています。そこそこ便利に使えています。ただ、Wi-Fiを使っているからか、PCの性能が低いからか、たまに画像の遅延がかなり大きくなります。
また、今回のコードはInternet Explorerでしか動かないので、Android端末でも動くものを作りたいですね。