2012/06/30

Google App EngineでWANアドレス管理サーバー(開発編)

Google App Engineサイトを活用して家のWANアドレス管理サーバーを構築する

開発環境は前回整えたので、サーバーアプリ作ってみた。と言ってもやることは
ごく簡単なことなのでAppEngineの知識はチュートリアルレベルで十分。

アプリケーションを定義するapp.yamlは以下の様な感じ。
[app.yaml]
application: アプリ名
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico

- url: .*
  script: main.app
#  login: required

libraries:
- name: webapp2
  version: "2.5.1"
サンプルをコピペしてapplication: のところを変更しただけかな。

アプリにアクセスした時に起動するメインプログラムがこちら。
[main.py]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import webapp2
from google.appengine.api import users
from google.appengine.ext import db
from datetime import datetime,timedelta


class AppUser(db.Model):
    account = db.UserProperty()
    lanaddr = db.StringProperty()
    address = db.StringProperty()
    apppath = db.StringProperty()
    updated = db.DateTimeProperty()


class RootHandler(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write( self.request.remote_addr )


class MyHandler(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/html'

        user = users.get_current_user()
        if user:
            self.response.out.write(''+user.email()+'')

            app_users = AppUser.gql("where account = :1",user)
            for au in app_users:
                if self.request.remote_addr == au.address:
                    url = "http://"+au.lanaddr+au.apppath
                    non = "http://"+au.address+au.apppath
                else:
                    url = "http://"+au.address+au.apppath
                    non = "http://"+au.lanaddr+au.apppath
                self.response.out.write(""+url+"
")
                self.response.out.write(non+"
")
        else:
            self.redirect(users.create_login_url(self.request.uri))


class CommitHandler(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        user = users.get_current_user()
        if user:
            app_users = AppUser.gql("where account = :1",user)

            if app_users.count() == 0:
                ip = AppUser()
            else:
                ip = app_users[0]

            ip.account = user
            ip.updated = datetime.now()
            ip.address = self.request.remote_addr
            ip.apppath = self.request.get('path')
            ip.lanaddr = self.request.get('lan')
            if ip.lanaddr == "":
                ip.lanaddr = "localhost" 
            ip.put()
            self.response.out.write(ip.address+" "+ip.lanaddr+" "+ip.apppath+" COMMITED")
        else:
            self.redirect(users.create_login_url(self.request.uri))


class AdminHandler(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        user = users.get_current_user()
        if user:
            if users.is_current_user_admin():
                self.response.out.write('AppUsers')
                for u in AppUser().all():
                    self.response.out.write("\n"+u.account.email())
                    self.response.out.write(" "+u.lanaddr)
                    self.response.out.write(" "+u.address)
                    self.response.out.write(" "+u.apppath)
                    self.response.out.write(" "+str(u.updated+timedelta(hours=9)))
        else:
            self.redirect(users.create_login_url(self.request.uri))

            
app = webapp2.WSGIApplication([('/', RootHandler),
                               ('/my', MyHandler),
                               ('/commit', CommitHandler),
                               ('/admin', AdminHandler)
                               ],
                              debug=True)
サンプルをコピーして、必要なコードを書き加えていったところ、こんななりました。
Apacheライセンスである事が記述されてますが、サンプルがそうなっていたのでそのまま残してます。自分はそんなつもりはないんだけど。

順を追って説明しておくと

import webapp2
'webapp2'フレームワークを使いますってこと。サンプルがそうなってた。

from google.appengine.api import users
ユーザサービスというアカウント認証関係のAPIを使用する。WANアドレスをこのアカウントと紐付けるため。

from google.appengine.ext import db
データストアというデータベースを使用する。これ使ってデータ管理する。

from datetime import datetime,timedelta
これがちょっと悩んだんですが、サーバー側の時間が全部UTCなんですね。そこからJSTへ変換するための最小モジュールをインポート。pythonモジュールで時間関係の便利なモジュールは沢山あるけれど、やりたいことはたった一つ
self.response.out.write(" "+str(u.updated+timedelta(hours=9)))
の部分なので、timedelta使うことに。
この辺は、'google app engine utc jst'でググれば沢山親切ページが現れる。

以下の様な定義を入れておくことで、データストアにAppUserという自分専用のデータテーブルが作成される

class AppUser(db.Model):
    account = ユーザアカウント(Googleアカウントオブジェクトインスタンス)
    lanaddr = LANアドレス(コミット時に自分でパラメータ指定する)
    address = WANアドレス(self.request.remote_addrで得られる。目的の物!)
    apppath = 自分ちのサーバーアプリへのURL(コミット時に自分でパラメータ指定する)
    updated = コミット時の更新日時
プログラム上では、この'AppUser'クラスを使ってDBへアクセスする。

最初に実行されるフレームワークアプリケーションオブジェクト生成部分でハンドラの定義をする。

app = webapp2.WSGIApplication([('/', RootHandler),
                               ('/my', MyHandler),
                               ('/commit', CommitHandler),
                               ('/admin', AdminHandler)

http://[app-name].appspot.com だったら、RootHandler
http://[app-name].appspot.com/my だったら、MyHandler
http://[app-name].appspot.com/commit だったら、CommitHandler
http://[app-name].appspot.com/admin だったら、AdminHandler
で、それぞれの'get'メソッドが呼ばれる。Railsでいうところのroutesかな。

class RootHandler(webapp2.RequestHandler):
ここはとにかく単純にWANアドレスを返す。そういうサイトよくあるよね。
バッチ処理とかで使いやすいようにプレーンテキストでアドレスのみを返すようにした。結構便利。

class MyHandler(webapp2.RequestHandler):
自分のアドレスを返すところ。
ログインユーザのアカウントに紐付いたデータが登録されていれば、それをリンクで表示。これがやりたかっただけなんだけど、道のり長いっす。
ログインしていないなら、ログインページへ飛ばす。ログインしたら戻ってくる。
self.redirect(users.create_login_url(self.request.uri))
がフレームワークとユーザサービスの便利な組み合わせ。
これもチュートリアルの範囲内。

class CommitHandler(webapp2.RequestHandler):
"http://[app-name].appspot.com/commit?lan=XXXX&path=YYYY" ってアクセスする。
アカウント認証されたユーザならば、DB検索してマッチしたデータエンティティに、なければ新たなエンティティにデータを記録するところ。ログインしてなければ同じようにログインページへ飛ぶ。

class AdminHandler(webapp2.RequestHandler):
ここはデータテーブルの一覧を表示するところ。管理者(自分)のみが観覧できる。

チュートリアルに従って作って、ブラウザ上からアクセスして機能テスト。問題なし!

実際にはここまで作るだけでも試行錯誤したけど・・・本題とは遠いところで詰まった。
例えば、UTC=>JSTのシンプル解決に悩んだりとか。
適当なモジュールをインポートしてもサーバーになかったりとか。
途中からDBモデルにlanaddrを追加したんだけれど、なぜか更新されないとか。
'https://appengine.google.com'のアプリ管理からデータテーブルを削除したり、ログ見たり・・・

だけど、一番苦労したのは、ログイン認証だ。
サーバーは出来たが誰でもアクセスできちゃう。管理ユーザ(自分)だけしかアクセスできないパーミッション設定が可能だけど、それはそれで面倒だ。
個人のデータを保持するにはやはり個人を特定する認証が必要なのだ。
(もし利用希望者がおられれば、URLお教えしますが・・・)

ブラウザからならログインページが出て、ログインしたら後はブラウザがよろしくやってくれるけど、やりたいのは自分ちのUbuntuServerから、cronタスクで処理したいので、ブラウザがやってることを自分でやらんとならん。

googleアカウント認証は、こちらを参考にしましたよ
http://johannilsson.com/2011/04/13/authenticated-requests-on-app-engine.html
http://maimon-it.blogspot.jp/2010/04/authenticating-google-app-engine-apps.html

スクリプトでのログイン認証と、認証付きアクセスを記述したものがこちら
[gauth.py]
#!/usr/bin/python
# -*- coding: utf-8 -*-

from subprocess import *
from os import path

mail = 'Googleメールアドレス'
password = 'パスワード'
app = 'アプリ名'

app_url = 'http://'+app+'.appspot.com/'
cookie_file = path.expanduser("~")+'/.google_myapp_cookie'

def login():
    try:
        # API認証キーを取得
        print 'Get Auth-Key...\n'
        auth = Popen(['curl','-f','-s',
                      'https://www.google.com/accounts/ClientLogin',
                      '-d','accountType=HOSTED_OR_GOOGLE',
                      '-d','Email='  + mail,
                      '-d','Passwd=' + password,
                      '-d','source=' + app,
                      '-d','service=ah'],
                     stdout=PIPE).communicate()[0]
        # ログインしてクッキー取得
        print 'Login...\n'
        login = Popen(['curl','-c','-',
                       app_url+'_ah/login?auth='+auth[ auth.rindex("Auth=")+5:-1]],
                      stdout=PIPE).communicate()[0]
        cookie = login[login.rindex("ACSID")+6:-1]

        # クッキーの保存
        f = open(cookie_file,'w')
        f.write(cookie)
        f.close()
        return cookie
    except:
        return ""

def get(controller):
    try:
        f = open(cookie_file)
        cookie = f.read()
        f.close()
    except:
        cookie = login()

    try:
        return Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
                     stdout=PIPE).communicate()[0]
    except:
        return ''

pythonのsubprocessや、外部コマンド'curl'については、親切解説しているところが沢山あるんで。そちらで、よろしくです。

処理内容を簡単に説明する

loginメソッド:(ログインしてクッキーを得るまで)

'https://www.google.com/accounts/ClientLogin' へ所定のパラメータ付きでアクセスしてアカウント認証データを取得。
'http://[app-name].appspot.com/_ah/login?auth=認証データのAuth=以降のデータ列' ってやるとログイン出来る。
ログインした時に得られるデータ内に"ACSID"というキーワードのデータ列があるんで、その部分を切り出して保存。
これがこのサイトへのログインクッキーデータ。
ドキュメントによると、Googleアカウントだけじゃなく、OpenIDでの認証も出来るらしいけど、その辺は無視。

getメソッド:(クッキー使ってアクセス)

クッキーデータをファイルから読んで、認証付きで指定URLへGETアクセスする。
手順がやっつけっぽいが、一応機能する。
Cookieが無効になる期限がどれくらいなのかイマイチ不明なので、その辺調べてはっきりしてから、ちゃんとした手順に直そうかな。
・・・
ということで、修正した getメソッドがこちら

def get(controller):
    for i in range(2):
        try:
            f = open(cookie_file)
            cookie = f.read()
            f.close()
            ret = Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
                        stdout=PIPE).communicate()[0]
            if ret.find('COMMITED')>0:
                return 'ok'
            else:
                raise
        except:
            cookie = login()
    return 'err'

Cookieが無効になるのは、https://appengine.google.com から、アプリ設定ページでCookie Expirationというところで設定するのだった。1日、1週間、2週間が設定できる。デフォルトは1日だった。最長の2週間に設定したがそれでも短いので、コミットに失敗したらログインからやり直しするように変更した。

cronで実行するスクリプト
[gcommith.py]
#!/usr/bin/python
# -*- coding: utf-8 -*-
import gauth
# コミット
#gauth.get('commit?lan=192.168.X.Y&path=/foo')
gauth.get('commit?lan=192.168.X.Y')

cronタスク
crontabでユーザレベルタスクで回してます。
$ crontab -e
ってやって、自分の好きなエディタで開いたら

0 3 * * * python ~/Grive/gcommit.py

って書いておく。毎日3時にコミット実行です。crontabで回すと自分にメールでタスク結果が飛んでくる。開発中は飛んでくるのが便利だけど、落ち着くとうざいね。止めちゃおっかな。

この記事をよく読んだ奇特な方はおや?と思ったかも。
gcommit.pyの冒頭に、#!/usr/bin/pythonって書いてあるのに、なぜに「python ~/Grive/gcommit.py」なのかと。その謎は、パス名[/Grive]に隠されております。
どうでも良い話ですが・・・次回はその話にちょっと脱線します。

2012/06/27

Google App EngineでWANアドレス管理サーバー(開設編)

自分の元wavecastマシンへ外からアクセスするには誰かがWAN側のIPアドレスを管理する必要がある訳だけど、今まではwavecast.tvがその役割をしていた。けれど、UbuntuServerで再構築してしまったので、自分でなんとかしないと。
固定IPとかDDNSではお金がかる。

そこで思ったのが、今流行のクラウドサービスを利用すれば無料で出来るんじゃん?と。
で軽く調べると世の中には沢山似たようなサービスがあったとさ。ありすぎてよくわからないので、自分に一番身近で無料で気軽に使えそうなのが、Goole App Engine だった。

ここで使えるスクリプト言語は日頃使っているJavaやPythonで書けるらしいと。
(ほんとはRubyで書きたいけど。実はそういう環境を作れたりするんだなこれが)
無料で10個の独立したアプリケーションを作れるらしいと。
MySQLじゃないけど、もどきSQLDBっぽいものが使えるらしいと。
Cronタスクも走らせられるらしいと。
日本語ドキュメントも分かりやすい。いけそうだ。

プランとしては、
app-engine上の自分のアプリにcronとかで定期的に元wavecast(名前が欲しいな・・)でアクセスして、そんときのWANアドレスをアプリ側のDBで覚えておいて、アプリにブラウザでアクセスしたら、WANアドレスでのURLが出てくるようにすればよい。
wavecast.tvとイメージは同じだ。

サイト開設のために必要なもの
・当然googleアカウントがいる。
・携帯キャリアのメールアドレスがいる。うーん。これがいやだ。

アプリケーション作成(サーバー)
appengine.google.comでアプリケーション名を決めてアプリ作成。
ここで作成した名前はhttp://アプリ名.appspot.comでアクセスできる。
自分は.comドメインを持ってるんで頭を同じ名前をつけた。
早い者勝ちなので、何はさておき、作っておきます。

SDKダウンロード
ローカルでアプリ開発するためのライブラリとアップロード・ダウンロードするツール一式が入ったSDKをダウンロードする。JavaにするとJRubyが使えるようになるらしいけど、今んところPythonじゃないとサポートしていないサービスがあるらしいのでPythonにした。


そうそう。Mac/Windows版のSDKを使えばGUIで便利にできるらしい。
私は端末一つで十分なので使わない。試しにWindows版を入れてみたけどMinGWやCygwinとか色々入ってるためかまともに動かなかった。やっぱりConsoleが一番!


アプリケーション作成(ローカル)
チュートリアルにそって、ざっと流れを把握する。これってRailsみたいだ。
全くその通りのことを説明されているぺーじもありました→appengineでRoR
チュートリアルのHelloWorldはせっせと必要なファイルを手作業で作る説明になってるけど、SDKに、'new_project_template'があるんで、これをコピって使うのが手っ取り早い。
アプリ名と同じ名前のディレクトリを作って、そこに放り込む。

app.yaml
application: サーバー側に作成したアプリ名を記述

main.py
あとは、ここのgetメソッドに色々書けばアプリとして動作する。

動作テスト
$ dev_appserver.py [app-name] で、テストサーバーを起動する。
http://localhost:3000でアクセスして動作を確認。

アップロード
$ appcfg.py update [app-name] で、サーバーにアップロード
動作テストが終わったら、本物へアップロードして動作確認。

サイト開設から、アプリ動作確認まで出来たので、次回は目的のものを作ってみる。

2012/06/22

ひとまず録画できるところまで構築

デバイス周りのセットアップを先に終わらせておこうと思って、録画が出来るところまで再構築です。

recfsusb2n
wavecast独自のtsrec2ではなく、付属地デジチューナの本家からいただきました。
セットアップドキュメント通りにやっていきます。

boostライブラリが必要らしい
$ sudo apt-get install libboost1.48-all-dev
boost::threadとfilesystemだけでいいらしいけど、面倒なので全入れ。


録画ユーザをvideoグループに追加
$ sudo gpasswd -a [username] video

/lib/udev/rules.d/89-tuner.rulesを作成して以下を記載
# FSUSB2N
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTRS{idVendor}=="0511", ATTRS{idProduct}=="0029", MODE="0664", GROUP="video"
変更を適用する。
$ sudo udevadm control --reload-rules

USB機器を認識していることを確認
$ lsusb -d 0511:
Bus 001 Device 004: ID 0511:0029 N'Able (DataBook) Technologies, Inc. 
って出た。問題なく認識しているようだ。

recfsusb2nをビルド
ビルドエラーが出る。こちらを参考にしてソースをちょっと手直し。
そうそう、後で困るので、こちらのパッチもやっておこう。
実際どうなるんだろ?と思ってオリジナルのままやってみたら確かにffmpegに怒られました。因にwavecastパッチでは10カウントでしたね。 
動作確認
$ recfsusb2n -bv 27 40 test.ts

以上の事を、ぶっ続けでやったら、パーミッションエラーになった。
自分が、videoグループに入ってることを認識させるため、一度ログインし直す。
OK。

ffmpeg環境を前回と同じようにgitリポジトリから構築
まずは、下ごしらえ
$ sudo apt-get install git
$ sudo apt-get install yasm
$ sudo apt-get install libfaac-dev
$ sudo apt-get install libopencore-amrnb-dev
$ sudo apt-get install libopencore-amrwb-dev
$ sudo apt-get install libtheora-dev
$ sudo apt-get install libvorbis-dev
$ sudo apt-get install libxvidcore-dev
$ sudo apt-get install libmp3lame-dev ←自分は入れなかった。mp3使わないし。

x264ビルド
$ git clone git://git.videolan.org/x264.git x264
$ cd x264
$ make
$ sudo make install


ffmpegビルド
$ git clone git://github.com/FFmpeg/FFmpeg.git ffmpeg
$ cd ffmpeg
$ ./configure ...前回と同じ設定。ただし、--enable-libmp3lame 外した。
$ make
$ sudo make install

MP4Box
mp4boxが含まれているgpacをインストール

$ sudo apt-get install gpac
$ MP4Box -ipod -inter 500 in.mp4 out.mp4 な感じ?

で、適当に作った録画スクリプトがこちら
#!/bin/bash

TMP=$(tempfile)
FILE=`echo $TMP|sed s/^.*[/]//`
VIDEO='/home//media/video'
IPOD='/home//media/ipod'

recfsusb2n --b25 $C $T ${TMP}.ts

ffmpeg -y -i ${TMP}.ts -c:v libx264 -c:a libfaac -preset superfast -b:v 1800k -s 960x540 -aspect 16:9 -f mp4 -threads 0 ${TMP}.mp4

MP4Box -ipod -inter 500 ${TMP}.mp4 -out $VIDEO/${FILE}.mp4

ffmpeg -y -i $VIDEO/${FILE}.mp4 -threads 0 -s 480x272 -acodec copy -vcodec mpeg4 -b:v 900k -qmin 3 -qmax 5 -f mp4 $IPOD/${FILE}.mp4

# Cleanup
rm ${TMP}*

つい最近のffmpegだと、ビデオのビットレートを、'-b'じゃなくて、'-b:v'で指定しなさいとワーニングが出るようになったので、'-b:v'に変更しました。オーディオのビットレート指定は、'-b:a'だそうだ。

時々、ffmpegとx264のリポジトリアップデートと再ビルドをしていれば最新が保たれる。
ただし今回みたいにオプションが変わったりするんで、自動化は難しいかな。
エンコードメッセージを解析して自動対応なんてことが出来ればメンテフリーになるか?

ここまで出来れば、あとは epgrec 様を入れればほぼ出来上がり。
今回は自力で色々組んでみたいと思っているので、モチベーションキープのためにも入れないで我慢です。

wavecast に Ubuntu 12.04 LTS インストール

出来る限り最新環境で色々構築してみたくて、やってしまいました。
これで、wavecastがwavecastじゃなくなってしまうけど、まあいいや。


いつものように、インストール手順を書き記しておきます。


64bit Ubuntu serverのISOイメージをダウンロード。
前にも書いたけれど、デスクトップ環境は必要ないので。Server版を入れます。

wavecastのCPUは、Intel Atom D525 なので、IA64です。
http://releases.ubuntu.com/precise/ ←ここから64-bit版のUbuntuServerのISOイメージをダウンロードします。
64-bit PC (AMD64) server install CD ←これね。

wifiドライバ
CD/DVDに焼いてドライブに入れて、いざリブート!そしていつものようにインストール。
順調です。おや?「フリーじゃないファームウエアファイルを必要としています」
だって。iwlwifi-1000-6.ucodeってのを読みたいらしい。NASでもUSBメモリでも何でもいいから入れて?って言われました・・・

探したら、http://intellinuxwireless.org/?n=downloads がヒットしたけど該当するファイルが入っているアーカイブがなかった。iwlwifi-1000-5.ucodeならあったけど・・・
wifiは使ってないから、とりあえずなしで。後からでも入れられるようだし。
wlanの人は、この辺よく調べてからにした方がいいね。

LVMなしでディスク全体を使う設定でインストール再開。
セキュリティ自動アップデートにした。やった事ない。平気かな・・・


OpenSSH、Mail、LAMP を選んでインストール。細かいものは後で手動で。

これといった問題もなくインストール終了。必要最小限なので立ち上がりも速いっす。

とりあえずの環境整備

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install emacs (私はこれがないと・・・)

build-essentialいれようとしたけど、gccってやったら、pentium-builder入れろって出たんでそうする。

$ sudo apt-get install pentium-builder
$ sudo apt-get install make

iMacからsshでアクセス
いつものようにアクセスしたら、RSA host key が変わってるって言われ、失敗した。
.ssh/known_hostsから該当する行を削除して新しいRSA keyを受け入れるようにする。

これで、最小限の環境まで復活です。ここまではCentOSより楽チンですね。


2012/06/15

podcastで使い分け

wavecast購入後色々試行錯誤して、家で見るときはブラウザから観たり、iTunesに自動登録された方をiPad/AppleTVで観たり、出先ではiPodTouch(1st)に同期させて観たり、やりたかった事はだいたい出来たかな。

ただ、使いにくい事が1つある!

前記事にも書いたが、リビング用高画質版とモバイル用低画質版の2つを同じようにiTunesへ登録しているためにサムネイルも名前も同じようだから分かりにくい。選ぶときもどっちがどっちやら。

podcast使えないかな? と思い立った。
iTunesムービーは高画質版。podcastを低画質版って使い分ければいいんじゃ?
そうすればiPodTouchの同期設定もpodcastだけにすればいいし。
管理フォルダも別だからメンテもしやすそうだし。
ということで、やってみた。


podcast

については、podcastを制作するを参考に、お勉強。RSSなのね。というかXMLなのね。


プランとしては、podcast用のRSSを出力するスクリプトを用意してiTunesへ登録する。
最近はepgrecで録画するのが多くなって来たので、そっち側に構築。epgrecは、smartyを使っているので、そのまま活用させていただく。

/epgrec/podcast.php
<?php
include_once('config.php');
include_once( INSTALL_PATH . '/DBRecord.class.php' );
include_once( INSTALL_PATH . '/Smarty/Smarty.class.php' );
include_once( INSTALL_PATH . '/Settings.class.php' );

$host = "http://192.168.X.X";

$settings = Settings::factory();
$dbh = @mysql_connect( $settings->db_host, $settings->db_user, $settings->db_pass );

$rvs = DBRecord::createRecords(RESERVE_TBL, "WHERE complete='1' and mode>1");
$records = array();
foreach( $rvs as $r ) {
    $cat = new DBRecord(CATEGORY_TBL, "id", $r->category_id );
    $ch  = new DBRecord(CHANNEL_TBL,  "id", $r->channel_id );
    $arr = array();

    $r->title = str_replace('【デ】','',$r->title);
    $r->title = str_replace('【二】','',$r->title);
    $r->title = str_replace('【字】','',$r->title);

    $arr['title'] = $r->title;
    $arr['author'] = $ch->name;
    $arr['subtitle'] = "";
    $arr['description'] = $r->description;
    $arr['thumb'] = $host.$settings->install_url.$settings->thumbs."/".$r->path.".jpg";
    $arr['url'] = $host.$settings->install_url."/podcast/".$r->path;
    $arr['pubdate'] = $r->starttime;
    $arr['duration'] = strftime("%T",strtotime($r->endtime)-strtotime($r->starttime)-60*60*9);
    $arr['category'] = $cat->name_jp;
    array_push( $records, $arr );
}

$smarty = new Smarty();
$smarty->assign("site_title","録画一覧");
$smarty->assign("site_link",$host.$settings->install_url);
$smarty->assign( "records", $records );
$smarty->display("podcast.xml");
?>

/epgrec/templates/podcast.xml
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
  <channel>
    <title>{$site_title}</title>
    <language>jp</language>
    <itunes:category text="録画">
      <itunes:category text="TV録画"/>
    </itunes:category>

{foreach from=$records item=rec}
<item>
  <title>{$rec.title}</title>
  <itunes:author>{$rec.author}</itunes:author>
  <itunes:summary>{$rec.description}</itunes:summary>
  <enclosure url="{$rec.url}" type="video/mp4"/>
  <guid>{$rec.pubdate}</guid>
  <pubDate>{$rec.pubdate}</pubDate>
  <itunes:category>{$rec.category}</itunes:category>
  <itunes:duration>{$rec.duration}</itunes:duration>
</item>
{/foreach}

  </channel>
</rss>
を作成。(うまく入らないので、どちらも">","<"を全角にしてます

iTunesのpodcastに、http://192.168.?.?/epgrec/podcast.php で登録して出来上がり。家の中だけでいいので、LAN固定IPで問題なし。iTunesの方から勝手に吸い取ってくれるので自動登録フォルダにコピーするより、結果こっちの方が楽だね。

必要最小限のタグしか使ってない。もっと沢山あるみたいだけど、いらないかな。

今回はまったところは、2つ以上のアイテム(エピソード)が追加できなかったこと。最初の1つしかpodcastに出てこないのだ。エラーとかも出ないし・・悩んだよ。
結局、<guid>タグが必要だった事とその内容に日本語が入れられない事だった。
<pubdata>と同じ公開日(録画日)を入れる事にした。そうしたら出たよ。ふぅ。

Webプログラマじゃないんで知らない事だらけ。実際には色々試行錯誤があったけれど、やってみると簡単で面白い。知ってる人にすれば、この辺は既に古い話なんだろうね。実際ネット上を調べてみても数年前の情報がほとんど。今から追いつけるかなぁ。


2012/06/12

続ffmpegでどうにかする

前回記事で、ffmpegでどうのこうのと書いたけど、プリセット'fast'だと遅すぎて実用的じゃなかった。
fastなのに遅すぎるって、このプリセット書いた人ってのはどんなマシンスペックを標準と考えてるんだろ?まあ、細かいことは置いといて、実用的な処理時間でそこそこの品質をターゲットにしてみるかと。

計測

Wavecast標準フローとどっちが効率的かな?というのも気になったので、計測してみた。
30分番組をそれぞれのパターンで計測した。計測は'time'コマンドを使用した。
沢山サンプリングした平均ということじゃないんで、今回の結果ではということで。
プリセット以外のエンコード条件、その他は前記事どおり。

 elapsed
Wavecast標準フロー(vpre ibento7)
tssplit 0:01:15
demux 0:02:30
ffmpeg 0:19:59(video)
ffmpeg 0:04:35(audio)
mp4box 0:00:21
合計 0:28:40
fast
ffmpeg 1:38:58
mp4box 0:00:34
合計 1:39:32
veryfast
ffmpeg 0:50:32
mp4box 0:00:37
合計 0:51:09
superfast
ffmpeg 0:22:57
mp4box 0:00:34
合計 0:23:32
superfast + yadif
ffmpeg 0:53:07
mp4box 0:00:30
合計 0:53:37

結果
Wavecastフローはドキュメント通り録画時間と同じくらいの時間であった。
fastだと3倍以上!、veryfastでも倍近い時間がかかった。superfastでやっと現実的な処理時間だ。やっぱりそのくらいのハードスペックってことか。
そうそう、最近気づいたんだけど、標準フローで使われているibento7プリセットって、superfastと内容が同じだ。

yadif
最後の'superfast + yadif'のyadifは、Deinterlaceフィルタのこと。ffmpegのドキュメントを読むと-deinterlaceではなく、yadifフィルタを使いなさいということだ。指定するときは、'-vf yadif'ってやる。
結果はご覧のとおり倍近い時間がかかった。

なんでdeinterlaceしたかというと、映画やアニメはもともとプログレッシブだから大丈夫だけどニュースとかスポーツとかだと60fps録画ナマだったりするんでインターレースが気になる。やはり映像ソースやどう残したいかに合わせて使い分けかな。

結論
'superfast'でいいか。若干スピードについていけてない感があるんでスポーツ番組には不向きだけど、観れないってほどじゃない。ということで、標準フローより若干処理時間を短縮できたということかな。
逆に言えば、標準フローは思ったより非効率じゃないってことだね。ただ音ズレがね。

2012/06/06

最新ffmpegでどうにかする


wavecast標準システムも epgrec も、 なんとなく落ち着いてきたが、たまに録画結果にがっかりする時がある。途中から音と映像が大きく(数秒)ズレこんでしまうのだ。これをどうにか解消したい。

現在の手順
  1. TSからHDストリームだけを抽出
  2. HDTSからビデオとオーディオをdemuxして分離
  3. ビデオ(m2v)をH264にエンコード
  4. オーディオ(wav)をsoxでズレを微調整
  5. オーディオ(wav)をfaacにエンコードと同時にビデオとミックス
  • 最後の合成段階で同期処理が適切に行われない場合があるんじゃないか?
  • -vsync 1オプションで同期されるというが、壊れるケースがあるんじゃ困る。
  • 分離した方が機能がフルに使えて都合がよいというが、地デジ録画という特定条件に対して、どれほどの意味があるのか、自分はよく分からない。
ということでエンコードフローを見直すことにした。根拠が薄いので、これも実験だけど。

プランとしてはこんなイメージ
  • フローを最小化することで途中の問題点を回避できるかも
  • 最新ffmpegを使用することで不具合が解消できるかも
  • 全体の処理時間が短縮出来ればそれはそれで喜ばしい
最初は楽してどうにかならないかと思って、apt-get でffmpegインストールしてみたけど、コンフィグが--enable-libfaacじゃなかった。h264とかfaacとかライセンスがめんどくさいんで、使いたい人は自分でコンフィグしろよって感じかな。ということで、ソースからビルドです。

wavecast-01(v1.8)に入っているffmpegのバージョンは「SVN-r26402」ですな。これはこれで古くはないけど、ffmpeg.orgへ行くと、SVNリポジトリはこのリビジョンで凍結。以降はGitリポジトリで開発するって。今後のメンテナンスも考えるとGitリポジトリで進めるほうがよさそう。

ffmpeg Gitリポジトリの入手  (色んなミラーがあるけど自分はここにした)
$ git clone git://github.com/FFmpeg/FFmpeg.git ffmpeg
これでffmpegにソース一式がダウンロードされる。リポジトリのアップデートは
$ cd ffmpeg
$ git pull
ってやれば現在の最新にアップデートされる。だけど最新≠安定じゃないから理解の上で。
ffmpegディレクトリ以下で、コンフィグ確認。

$ ./configure --enable-libx264 --enable-libfaac
libx264はGPLだから--enable-gplつけろって怒られる

$ ./configure --enable-libx264 --enable-libfaac --enable-gpl
libfaacはnonfreeだから--enable-nonfreeつけろって怒られる

$ ./configure --enable-libx264 --enable-libfaac --enable-gpl --enable-nonfree
libx264 version must be >= 0.118と怒られる

$ sudo apt-get install x264=0.118
とか、適当じゃ入らなかった。新しめのことはやっぱり面倒くさい。
まずはlibx264を入手して、ソースからビルド
$ git clone git://git.videolan.org/x264.git でリポジトリから入手。最近はgitが多いね。
コンフィグテストでyasmが無いよって言われたので
$ sudo apt-get install yasm
で入れる。
$ ./configure --enable-shared(アプリじゃなくライブラリが欲しいので)
$ make
$ sudo make install
/usr/local/libに入る。念のため、ldconfigやるか?いっか。
ffmpegへもどって
$ ./configure --enable-libx264 --enable-libfaac --enable-gpl --enable-nonfree
通った。そうそう、libfaacは最初から入ってたのを使う。これでビルド環境は整った。

本番コンフィグ

wavecastにインプリメントされているffmpegは以下のようにコンフィグされている
configuration: --enable-gpl --enable-version3 --enable-nonfree --enable-postproc --enable-libfaac --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-x11grab --extra-cflags='-mtune=core2 -mfpmath=sse -msse'
これを参考に、最新には使えなかった設定とx11grabとかいらないよ。と思うやつを消して
./configure --enable-gpl --enable-version3 --enable-nonfree --enable-postproc --enable-libfaac --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libtheora --enable-libvorbis --enable-libx264 --enable-libxvid --extra-cflags='-mtune=core2 -mfpmath=sse -msse' --enable-avresample
 な感じになった。最後の--enable-avresampleを追加したのは、ドキュメントを読むとasynctsオプションが使えるようになるらしいと、asynctsは音声をタイムスタンプに同期するためのものらしいと、今回の目的に合いそうなオプションなので入れてみた。

で、無事コンフィグが通ったら
$ make
$ sudo make install
で、ビルド&インストール。
アプリは/usr/local/bin、プリセットは/usr/local/share/ffmpeg に入る。

エンコードテスト

$ ffmpeg -y -i TEST.ts -c:v libx264 -c:a libfaac -vpre ibento7 -f mp4 TEST.mp4
(ibento7はwavecastで用意されているプリセット)
Unrecognized option 'directpred'
/home/wave/.ffmpeg/ibento7.ffpreset: Invalid option or argument: 'directpred=1
', parsed as 'directpred' = '1'
エラー出るじゃないですか!

$ ffmpeg -y -i TEST.ts -c:v libx264 -c:a libfaac -vpre libx264-fast -f mp4 TEST.mp4
(libx264-fastはffmpegの付属プリセット)

でも同じエラーが。

試しに、directpredオプションを外してみたが、別のエラーが出てダメなプリセットだと言われる。
これを質問している記事を何件か見つけたけど、めぼしい解答が付いてない。
が、ひとつ糸口を見つけた。-presetを使ってみよという記事が。
(何処の記事だったかわからなくなりました・・)

確かにドキュメントを読むと、-vpre,-apre,-spreの他に-presetがある。しかも
-preset libx264-fastじゃだめで、-preset fastって書くらしい。
ファイル名のプリフィックスはエンコーダ名に紐付いているようなのだが・・・

結局のところ、-presetは、x264の--presetのことらしい。
詳しくは’x264 --fullhelp’ってやんなさいってffmpegのヘルプに書いてある。ややこしい。

$ ffmpeg -y -i TEST.ts -c:v libx264 -c:a libfaac -preset fast -f mp4 TEST.mp4

おお。動いた! 
そうそう。-c:v -c:aは、-vcodec,-acodecの別の書き方。気分です。

-vpre -fpreが使えないっす。実際のところ、どうするんでしょ? 

カスタムプリセットはひとまず諦め。x264プリセットなら使えそうなので、方針変更。
最新で標準的な使い方は2012年版FFmpeg が詳しく書かれています。


で、なんだかんだと実験した結果、こうなった
 (以前書いた、epgrec/do-record.shから抜粋)
  1. /usr/local/bin/ffmpeg -y -i /tmp/${REC}.ts -c:v libx264 -c:a libfaac -preset fast -b 2000k -s 960x540 -aspect 16:9 -f mp4 -threads 0 /tmp/${REC}.mp4
  2. /usr/bin/MP4Box -ipod -inter 500 /tmp/${REC}.mp4 -out $OUTPUT
  • tssplit2でのHDストリーム抽出やめ。
  • tsdemux2でのvideo/audio分離やめ。
  • よって、sox調整なし
  • よって、video/audio合成なし
素のTSファイルからh264/faacのmp4を作成。MP4BoxでiPod対応。だけになった。

映像は?
自分にとっては、-preset fast -b 2000k で十分かつ現実的かな。ウソ。現実的じゃなかった。続編へ。
以前は-r 24とか入れてたけどスポーツ番組には不向きだったので外した。

サウンドは?
エンコード劣化があると、こもった感じになるんで、48kHz,128kbpsのまま。

同期は?
今のところ良い感じ。ちょっと音が速いのでは?という番組もあるが、数秒ずれてしまう現象は今のところ発生していない。まあこの辺はもう少し実績をつまないとだけど。

 -tune ってのがあるらしい
x264の--tuneへ渡すオプションがffmpegの-tuneらしい。-tune animation とか、-tune film とかやれるそうな。録画情報からタイプを識別して振り分けとか出来るかもね。

asyncts どうなった?
えー、良く分からん。というか全然分からん。ドキュメントによると、
‘compensate’‘min_delta’‘max_comp’という3つのパラメータがあるらしいのだが
$ ffmpeg -i TS -filter_complex asyncts=max_comp OUT-file
ってやってみたものの、max_compなんて知らんって言われる。
色々入出力条件とかあるのか?・・・忘れることにする。


様子見
/root/taskの方は今までどおり、epgrec/do-record.sh を実験台にして様子見。
特に悪くなさそうなら、/root/taskの方へも反映させよう。

なんてもっともらしいことを書いてまとめようとしているが、結果から別段最新ffmpegである必要はなかったのでは?とも思う。が、色々やるんだったらやっぱり新しい方を使いたかったのだよ。私はやっぱり新車に乗りたいのだよ。

2012/06/01

wavecast録画ファイル名をどうにかする

epgrecならばファイル名カスタマイズ設定が出来るんでいいんですが、wavecastシステムのほうだと「UHF-channnel-date-time-s.mp4」って感じなのです。普通にブラウザベースで視聴するだけなら別にこれでもいいんだけれど、iTunesへ登録してAppleTVとかiPadで見たりしているとファイル名が重要なんです。AppleTVだとサムネイルが出るまですごく時間かかったり、出なかったり・・・なのでね。

と言うことで、録画ファイルをiTunesへコピーする段階でファイル名を番組名にしようと思います。

データベース(MySQL)から抜いてくる


ブラウザで録画した番組リストを出した時の番組名とかは、MySQLのデータベースに記録されているので、そこから引っこ抜いてくればいいやと。

で、調べまして、wavecastのデータベースには、vidrecとかv_vidrecっていう録画した番組のレコードがあると。その中にvid_fileがファイル名、vid_nameが番組名らしいと。

そして作成したファイル名変換スクリプト”v_rename.py”が、こちら。

#! /usr/bin/python
# -*- coding: utf-8 -*-
import sys
import os
import re
path = sys.argv[1]
fname = re.sub(r'^.*/','',path) #パスを消す。os.path使う?
fname = os.path.basename(path) #パスを消す。
vname = fname = re.sub(r'-s.mp4$','',fname) #-s以降を消す


import MySQLdb
from MySQLdb.cursors import DictCursor


try:
    con = MySQLdb.connect(host="127.0.0.1",charset="utf8",
                          db="XXX",user="XXX",passwd="XXX")
    cur = con.cursor();


    if cur.execute("select vid_name from vidrec where vid_file='"+fname+"'") > 0:
        row = cur.fetchone()
        vname = row[0] + fname.split('-')[2] # 日付追加


except:
    cur.close()
    con.close()


print vname
os.rename(path,path.replace(fname,vname))
(db,user,passwdは自分のに合わせて下さい。まあ皆同じだろうけれど)

ファイルパスを受け取って、UHF??っていう名前だけにして検索して、ヒットした番組名に変換です。
直接os.rename()でやってますが、シェル上でやろうとすると日本語コードがらみで面倒くさいので変換までやります。ヒットしなければ同名のまま。っていう処理です。

pythonで書いてありますが、/root/task内処理がpythonベースなので揃えたまでです。
このままでも動きますが、ちょっとでも効率良く動いてもらうために
$ python -m compileall v_rename.py
$ chmod +x v_rename.pyc
ってやって、pycの方を使うようにします。

cpとmvの間か

作ったスクリプトを、/root/taskに入れておいて、以前作ったitunesへのコピースクリプトが

cp $1 /media/nas1/iTunes/
mv /media/nas1/iTunes/*.m* '/media/nas1/iTunes/iTunes に自動的に追加'
な感じなので、

cp $1 /media/nas1/iTunes/
python /root/task/v_rename.pyc /media/nas1/iTunes/UHF*
mv /media/nas1/iTunes/*.m* '/media/nas1/iTunes/iTunes に自動的に追加'
でいいかなっと。
コピー後のファイルパスをちゃんと作って渡すのが面倒くさいのでUHF*ってことにして続きはOSに補完してもらうことにしました。

これで利用しやすくなるかな。


wavecastでepgrec構築 視聴編

wavecast上でepgrec動いたので、やっぱり外からアクセスして観てみたいわけです

外からepgrecページヘアクセス

http://wavecast.tv/01/でいつものように自分のwavecastにログインして
http://(ip-address)/wave/・・・ってなるので
http://(ip-address)/epgrec にURL変更してepgrecページを表示。おぉ。出た出た。


録画済一覧ページが・・・

あらら。サムネイルがちゃんとでない。
HTMLソースを見ると・・・なるほど、ローカルIPアドレスからのURLになっている。
デフォルトのままなら「http://localhost/epgrec」かな。これでは外アクセスには不都合なので、何処かを直さなくては。
と、初めは考えてSettings.class.phpのfactory()関数で、return $obj;の直前で
$obj->install_url = "http://".$_SERVER["SERVER_NAME"]."/epgrec";
ってやれば、その時点でIPアドレスになっていいかと。実際これでもいけるけど、
せっかく設定できるようになっているのに"/epgrec"が決め打ちなのはどうなの?と思い直して、、、はたと気づいたよ。

epgrecのシステム設定で、インストールURLのところを、"/epgrec"だけにすればいいじゃん。後はブラウザが適当に補完してくれる。URLって言葉に惑わされておりました。

HTML5対応

epgrecでの視聴はasf(asx?)フォームを出力するようになっていて、PCでVLCプレイヤーとかで見るように書かれているみたいだが、wavecast同様、どんなプラットフォームでも視聴したいので、ここはちょっ手を加えてHTML5のvideoタグでの再生に対応させる。

viewer.php (再生スクリプト)を直接書き換えればいいんだろうけど、元ソースをできるだけいじりたくないので、別ファイルに。

viewer_html5.phpというファイルを作成して、再生時にこっちが使われるように
recordedTbl.php (録画一覧スクリプト)ないで、
$arr['asf'] = "".$settings->install_url."/viewer.php?reserve_id=".$r->id;

$arr['asf'] = "".$settings->install_url."/viewer_html5.php?reserve_id=".$r->id;
と書き換える。
これでサムネイルとかクリックするとviewer_html5.phpの方が使わる。
viewer_html5.php の中身は一応こんな感じ。
<?php
//「セッションの有効期限をプライベートに調整する」ってことらしい
ini_set('session.cache_limiter', 'private');
session_start();


//キャッシュを無効
header("Expires: Thu, 01 Dec 1994 16:00:00 GMT");
header("Last-Modified: ". gmdate("D, d M Y H:i:s"). " GMT");
header("Cache-Control: no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");


include_once("config.php");
include_once(INSTALL_PATH . "/DBRecord.class.php" );
include_once(INSTALL_PATH . "/Settings.class.php" );


$settings = Settings::factory();


if( ! isset( $_GET['reserve_id'] )) jdialog("予約番号が指定されていません", "recordedTable.php");
$reserve_id = $_GET['reserve_id'];


try{
$rrec = new DBRecord( RESERVE_TBL, "id", $reserve_id );
$src = $settings->install_url.$settings->spool."/".$rrec->path;
//$src = "/epgrec".$settings->spool."/".$rrec->path;
}
catch(exception $e ) {
exit( $e->getMessage() );
}
?>


<!doctype html>
<html lang="ja">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
  </head>
  <body style="padding: 0px; margin: 0px; background-color: black;" >
    <div style="vertical-align:middle;">
      <video src="<?php print $src ?>" width="100%" preload="none" autoplay autobuffer controls onclick="this.play();"/>
    </div>
  </body>
</html>

これで手持ちのデバイスから視聴が出来るようになりました!

しかし残念ながら、iOSとAndroidではオートプレイが効きませんな。色んなサイトの記事で書かれている方法を幾つか試してみたんだけどダメ。wavecastからは出来てるんだけどなぁ。この辺はもう少し勉強して試行錯誤するか。
それに結果的に一番良く使っているiPodTouch初代では見れない。対応させたソース設定とか必要なんだろうね。この辺も改良の余地あり。

WAVECASTメニューに統合

冒頭に書いた、http://(my-global-ip)/epgrec にURL変更してepgrecページを表示って手順を踏むのがとても面倒なのでWAVECASTメニューページからepgrecのページヘ飛ぶボタンを付けてみた。

/home/wave/wave/jp/menu.php に、以下の記述を追加しました。
具体的には、<div class="info">・・・</div>の後。
<div class="toolbar">
  <h1>EPGREC メニュー</h1>
</div>
<table bgcolor=black width=100%>
  <tr>
    <td><ul class=edgetoedge><li><a href=../../epgrec/recordedTable.php rel=external>録画リスト</a></td>
    <td><ul class=edgetoedge><li><a href=../../epgrec/reservationTable.php rel=external>予約リスト</a></td>
    <td><ul class=edgetoedge><li><a href=../../epgrec/programTable.php rel=external>番組検索</a></td>
    <td><ul class=edgetoedge><li><a href=../../epgrec rel=external>番組表</a></td>
  <tr>
    <td><ul class=edgetoedge><li><a href=../../epgrec/keywordTable.php rel=external>自動登録</a></td>
    <td><ul class=edgetoedge><li><a href=../../epgrec/envSetting.php rel=external>環境設定</a></td>
    <td><ul class=edgetoedge><li><a href=../../epgrec/systemSetting.php rel=external>システム設定</a></td>
    <td><ul class=edgetoedge><li><a href=../../epgrec/logViewer.php rel=external>動作ログ</a></td>
</table>

menu.phpからの相対パス(../../epgrec)でいける。
rel=externalを付けないと、AndroidやiOSだと飛んでくれないんだね。変な感じ。そういうことならと思って、target="_blank"ってやってみたら、いけるんだけど、今度はPCのブラウザだと別ページになっちゃう。なるほど、そういう事か。

wavecastのページはjQTouchで構成されているので、div,ul,liのclass指定で簡単にデザインを変更できて良い感じ。