仙石浩明の日記

2011年2月24日

決してBusyにならない SIP 留守番電話機を Perl で作ってみた 〜 用件が録音されるとメールに添付して送信

IP 電話でいろいろ遊ぼうとすると定番は Asterisk だが、 いかんせん牛刀すぎる。 個人的に VoIP を使う場合 PBX の必要はなく、 留守番電話や転送ができれば充分。 というわけでまずは留守番電話の機能をさくっと Perl で書いてみた (わずか 93行)。

sip_user: hiroaki_sengoku
sip_passwd: xxxxxxxx
sip_domain: ekiga.net
answer_rings: 30
answer_sound: /home/tam_ekiga/answer.raw
voicemail_time: 60
voicemail_dir: /home/tam_ekiga/LOCAL/voicemail

設定ファイル answer_machine.conf を ↑ のような感じで書いておいて、 留守番電話スクリプト answer_machine を実行する:

answer_machine --conf /home/tam_ekiga/answer_machine.conf

sip:hiroaki_sengoku@ekiga.net に着信があると、 応答メッセージを流した後、 相手の音声を録音し、 /home/tam_ekiga/LOCAL/voicemail ディレクトリに 8kHz サンプリングの 8bit μ-law 形式で保存する。 あとはこのディレクトリを監視するプログラムを走らせて、 新着録音をメールで送信するようにしておけばよい。

次に IPKall を利用 (申込には米国の IP アドレスからアクセスする必要あり) して電話番号をもらう。 米国ワシントン州リンデンの番号 +1-360-526-6699 が割当てられた。 上記 SIP 留守番電話が待受けている sip:hiroaki_sengoku@ekiga.net を、 この電話番号の転送先として IPKall に登録すれば、 Google Voice もどき (^^;) の出来上がり。 この番号に電話すると、 上記スクリプトが応答し (上記設定ファイルに answer_rings: 30 と書いてある通り、 30秒間呼び出しを続けないと応答しない)、 何かメッセージを吹き込めば mp3 形式の音声データが添付されたメールが私宛に届く。

この仕掛けを作った晩の翌日未明 3:16 に、 さっそく着信があり録音データが添付されたメールが届いた。 発信者の電話番号は +1-877-347-3760 だった (+1-877- は米国のフリーダイヤル)。 メールで送られて来た録音データがコレ。 英語が苦手な私にはちょっとつらい (^^;) が、 再生速度を遅めにして一生懸命聞き取ってみる:

Hello, this is a message for Korety Martin. If you are not Korety Martin, please hang up and call 877-347-3760 to remove this phone number from our records. If you continue to listen to this message, you are acknowledging that you are Korety Martin. This message contains personal and private information. There will now be a three second pause.

This is EOCCA, A Collection Agency. This is an attempt to collect a debt. Any information obtained will be used for that purpose. Please contact us about this important business matter at 877-347-3760. When calling please reference account number 9.

真面目にディクテーションするのが馬鹿馬鹿しくなるような、 絵に描いたような詐欺電話 (*_*)。 「Collection Agency」 は債権回収会社のこと。 「EOCCA, A Collection Agency」 で検索してみると、 いっぱい出てくる。 借金の心当たりがある人 (のうち 1% くらい?) は、 つい騙されて折り返し電話をかけてしまうのか。 日本とかだと人海戦術で電話をかけまくるらしいが、 自動で電話をかけまくるのが米国流なのか? 電話版スパムメールといった感じ?

なお、 最後がしり切れとんぼのような感じがするが、 これは録音時間を 60秒間に制限しているため (上記設定ファイルの voicemail_time: 60)。

- o -

以下は、 Perl プログラミングに興味があるかた向け:

CPAN に登録されている Net::SIP モジュールには、 サンプルプログラムとして留守番電話スクリプト answer_machine.pl が含まれている。 ところが、 このスクリプトだと着呼時にすぐオフフック (=受話器を持ち上げて電話に出る) してしまう。 ふつー留守番電話といえば、 呼び出し時間 (つまりベル^H^H着メロを鳴らす時間) を設定できるものだと思っていたが、 残念ながらこのスクリプトにはそのような機能はない。 これでは留守じゃなくても留守番電話が先に出てしまう!

着呼時にすぐオフフックするのではなく、 一定秒数待ってからオフフックするようにスクリプトを修正するなんて、 sleep 命令を書き加えるだけだろうと思うかもしれないが、 さにあらず。 Net::SIP は非同期処理が中心なので、 全ての処理はイベントループ Net::SIP::Dispatcher::Eventloop から呼び出される仕組になっている。 つまり各処理の中で勝手に sleep とかすると、 他の処理が全部滞ってしまう。 逆に言うと、 非同期処理なので一つのプロセスで複数の通話を扱える。 つまり決してお話し中 (Busy) にならない。

answer_machine.pl から引用:

use Net::SIP;
use Net::SIP::Util ':all';
	...
$ua->listen(
	init_media => [ \&play_welcome, $welcome,$hangup,$savedir ],
	...
	}
);
$ua->loop;

着呼待ちのためのスクリプトはたったこれだけ。 $ua->listen で、 オフフックしたときに呼び出される Callback (init_media) として \&play_welcome (関数リファレンス) を登録し、 $ua->loop でイベントループに入る。 着呼すると即オフフックして関数 play_welcome を引数 $welcome, $hangup, $savedir 付で呼び出す。 play_welcome は、 留守番電話の応答メッセージ (「ただいま留守にしております。 ご用件のあるかたはピーッという音の後にお話下さい」 など) を送信し、 相手が話す 「用件」 を録音する関数。

answer_machine.pl では自前の関数 play_welcome を定義して init_media へ登録しているが、 Net::SIP::Simple::RTP モジュール内にも media_send_recv という (応答メッセージを) 送信して、 (相手の用件を) 録音する関数が定義されている。 これを使えば、

$ua->listen(
    init_media => $ua->rtp('media_send_recv', 'answer.raw', 1, 'out.raw'),
    );

などと書くこともできる。 ここで answer.raw は 8kHz サンプリングの 8bit μ-law 形式の音声データのファイル名で、 これが応答メッセージとして再生され、 ファイル out.raw に同形式で相手の用件が録音される。

したがって、 呼び出し時間が常に 0 秒で構わなければ、

#!/usr/bin/perl
use Net::SIP;
my $user = 'hiroaki_sengoku';
my $pass = 'xxxxxxxx';
my $domain = 'ekiga.net';
my $ua = Net::SIP::Simple->new(
    outgoing_proxy => $domain,
    registrar => $domain,
    domain => $domain,
    from => $user,
    auth => [ $user, $pass ],
    ) || die;
$ua->register;
$ua->listen(
    init_media => $ua->rtp('media_send_recv', 'answer.raw', 1, 'out.raw'),
    );
$ua->loop;

これだけで留守番電話として機能する。

まったくもって簡潔すぎて、 どこをどう変更すれば呼び出し時間を確保できるのか、 これだけでは見当もつかないが、 関数 $ua->rtp (init_media に登録された Callback) が呼び出された段階で既にオフフックしてしまっているわけで、 時すでに遅しということだけは分かる。 どうやってオフフックする前に一定秒数待つ処理 (以下、Ringing と略記) を割り込ませばよいだろうか?

Net::SIP のドキュメントにはオフフック前の挙動については一切言及がないので、 仕方なく Net::SIP::Simple の listen のソースを読んでみる:

sub listen {
	my Net::SIP::Simple $self = shift;
	my %args = @_;

	# handle new requests
	my $receive = sub {
		my ($self,$args,$endpoint,$ctx,$request,$leg,$from) = @_;
		...
		# new invite, create call
		my $call = Net::SIP::Simple::Call->new( $self,$ctx,{ %$args });
		my $cb = UNIVERSAL::can( $call,'receive' ) || die;
		...
		# setup callback on context and call it for this packet
		$ctx->set_callback([ $cb,$call ]);
		$cb->( $call,$endpoint,$ctx,undef,undef,$request,$leg,$from );
	};

	$self->{endpoint}->set_application([ $receive, $self,\%args ]);
}

set_application は、 endpoint (Net::SIP::Endpoint, 通信端点) とユーザとをつなぐ application を endpoint に登録するための関数。 例えば着呼したら着信音を鳴らすとか、 ユーザが受話器を持ち上げたら着呼処理を行なうとか、 通信相手の音声をスピーカに出力してユーザに伝えるとか、 等々を行なう application (Callback) を登録する。

ここで登録される application である $receive (無名関数へのリファレンス) の処理内容を追っていくと、 実質的には Net::SIP::Simple::Call->receive を呼び出しているだけ。 で、この Net::SIP::Simple::Call->receive こそが実際の着呼処理、 すなわち通信相手からの INVITE リクエストに対して 「SIP/2.0 200 OK」 を返す処理を行なっている。

ということはつまり、 Net::SIP::Simple::Call->receive の呼び出しが行なわれる前に Ringing 処理を割り込ませばよい。 いろいろ方法はあると思うが、 endpoint に登録されている application を、 Ringing を行なう無名関数で置き換えてみた:

    my $sub = $ua->{endpoint}->{application}->[0];
    $ua->{endpoint}->{application}->[0] = sub {
	my ($self, $args, $endpoint, $ctx, $request, $leg, $from) = @_;
	my $meth = $request->method;
	if ($meth eq 'INVITE') {
	    my $res = $request->create_response('180', 'Ringing');
	    $endpoint->new_response($ctx, $res, $leg, $from);
	    $self->add_timer($rings, [$sub, $self, $args,
				      $endpoint, $ctx, $request, $leg, $from]);
	} else {
	    $endpoint->close_context($ctx);
	}
    };

これで着呼時にオフフックせずに、 Ringing を行なうことになる。 つまり、 「180 Ringing」 レスポンスを返し、 一定秒数 ($rings 秒) 後にオフフックさせるために add_timer を呼ぶ。 add_timer は、 第一引数 ($rings) で示す秒数後に、 第二引数 ([$sub, ...]) を Callback として呼び出すタイマーを登録する関数。 第二引数に、 元々 endpoint に登録されていた application へのリファレンス $sub を指定すれば、 $rings 秒後にオフフックして留守番応答が行なわれる。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 19:52

5件のコメント »

  1. 早速試してみました。使っているNet::SIP::Utilのバージョンが違うのか、1通話終わるごとに
    「Can’t use an undefined value as a HASH reference at /usr/local/lib/perl5/site_perl/5.8.9/Net/SIP/Simple/Call.pm line 347.」と言われて終了してしまいます。
    オリジナルのanswer_machine.plでは出ないので、sub answeringの処理をもう少し読んでみます…(短いので何度も読んでみたのですが、問題を見つけきれてません。ごめんなさい。)

    コメント by utaani — 2011年2月25日 @ 12:05

  2. バグ報告ありがとうございます。
    通信相手から通話を切るケースを試すのを忘れていました。
    &answering にて録音時間を制限するために add_timer を呼んでタイマーを登録しているわけですが、相手が通信を切ったときにこのタイマーを止める必要がありました。

    answer_machine.pl にならって recv_bye Callback でタイマーを止めるように answer_machine を修正しました。

    コメント by hiroaki_sengoku — 2011年2月25日 @ 15:18

  3. %perl -e ‘use Net::SIP; print $Net::SIP::VERSION’ ;
    0.62%

    sub answeringの中ではなく、コールバックを登録する部分でbyeの処理が抜けている感じですかね。オリジナルのanswer_machine.plよりコピーして

    52 $ua->listen(
    53 init_media => [ \&answering],
    54 recv_bye => sub {
    55 my $param = shift;
    56 my $t = delete $param->{stop_rtp_timer};
    57 $t && $t->cancel;
    58 }
    59 );

    としてうまくいきました。

    コメント by utaani — 2011年2月25日 @ 16:53

  4. あああ、すでに改修済みでしたか。失礼しました。(ブラウザをリロードして気づいた)

    コメント by utaani — 2011年2月25日 @ 16:53

  5. はじめまして。最近ekigaを使い始めた素人です。
    ekigaで留守電機能を使いたく調べていたらこのサイトにたどり着きました。しかし、記事で紹介されているスクリプトを実行するには具体的にどうしたらいいかよく分かりません。もしよろしければ教えていただけないでしょうか?

    コメント by BCD — 2013年8月28日 @ 10:36

この投稿へのコメントの RSS フィード。

コメントする