Perl で RTMP クライアントを書いてみた

なんでそんなものを。まさか Perlflash player を作ろうなどということは考えてなくて、単に Flash Media Server (うちにあるのは古い FCS-1.5 だけど) の死活監視をしたかった。
# exe 化した SWF を Windows XP で動かしてチェック、とかしてたんですが XP のほうがサーバよりはるかに安定しないもので

PerlRTMP ということなら Kamaitachi、ということで github で fork して Kamaitachi::Client を作ってみました。最初は別の名前空間にしようかとも思ったんだけど、やはり共通部分が多いので。
http://github.com/fujiwara/kamaitachi/tree/master

使い方。

コールバックを定義したクライアントを用意。

package MyClient;
use Moose;
extends "Kamaitachi::Client";
__PACKAGE__->meta->make_immutable;
no Moose;
sub on_invoke_onStatus {
    my $self   = shift;
    my $packet = shift;
    my $args = $packet->args;
    $self->logger->debug("onStatus.code=" . $args->[1]->{code});
    if ( $args->[1]->{code} eq 'NetStream.Publish.Start' ) {
        $self->logger->info("publish started successfuly.");
        $self->send_packet(
            Kamaitachi::Packet::Function->new(
                number => 3,
                type   => 0x14,
                id     => 5,
                method => "closeStream",
            ),
        );
    }
    else {
        $self->logger->error("publish started failed. exit");
    }
    $self->stop;
}

送りつけるパケットを Kamaitachi::Packet またはバイナリ列で用意して $client->run(@packet) するインターフェース。使いにくいような気もするけど……「なんか動かない」って時には本物の Flash player が送ってるパケットをキャプチャして、それをバイナリでそのまま送りつけてやる、みたいなことも可能。

my @packets = (
    Kamaitachi::Packet::Function->new(
        number => 3,
        type   => 0x14,
        id     => 1,
        method => "connect",
        args   => [],
    ),
    Kamaitachi::Packet::Function->new(
        number => 3,
        type   => 0x14,
        id     => 3,
        method => "createStream",
        args => [],
    ),
    Kamaitachi::Packet::Function->new(
        number => 3,
        type   => 0x14,
        id     => 4,
        obj    => 0x01000000,
        method => "publish",
        args   => [ undef, "test" ],
    ),
);
my $client = MyClient->new({
    url => "rtmp://localhost/stream/live",
});
$client->run(@packets);

この例だと、handshake してから connect, createStream, publish を送信して、onStatus が code eq 'NetStream.Publish.Start' で返ってきたら closeStream 送りつけて終了、という流れ。
デフォルトの動作 $client->auto(1) だと、サーバから packet_invoke が来たら次のパケットを自動的に返信するので、自分で送りたい場合は auto(0) にして $client->send_packet($packet) とか $client->send_next_packet() とか。

FCS-1.5 に対して実行したログはこんな。Data::Hexdumper でパケットの hex dump をみられるようにしました。送信パケットは全体、サーバからの受信パケットは $packet->data 部分。

[info] host: fcs port: 1935 app: sample 
[debug] on_write_ready 
[debug] on_read_ready handshake 
[debug] recieved packet length 3073. 
[debug] server token recieved. 
[debug] client token recieved. 
[debug] client token validate ok. 
[debug] send handshake packet. 
[debug] sending packet. type: 'packet_invoke' method: 'connect' 
[debug] 
  0x0000 : 03 00 00 00 00 00 F5 14 00 00 00 00 02 00 07 63 : ...............c
  0x0010 : 6F 6E 6E 65 63 74 00 3F F0 00 00 00 00 00 00 03 : onnect.?........
  0x0020 : 00 0B 61 75 64 69 6F 43 6F 64 65 63 73 00 40 A8 : ..audioCodecs.@.
  0x0030 : EE 00 00 00 00 00 00 07 70 61 67 65 55 72 6C 05 : ........pageUrl.
  0x0040 : 00 03 61 70 70 02 00 06 73 61 6D 70 6C 65 00 0B : ..app...sample..
  0x0050 : 76 69 64 65 6F 43 6F 64 65 63 73 00 40 6F 80 00 : videoCodecs.@o..
  0x0060 : 00 00 00 00 00 05 74 63 55 72 6C 02 00 15 72 74 : ......tcUrl...rt
  0x0070 : 6D 70 3A 2F 2F 70 2D 63 68 69 6E 61 2F 73 61 6D : mp://p-china/sam
  0x0080 : 70 6C 65 00 06 73 77 66 55 72 6C 05 C3 00 0D 76 : ple..swfUrl....v
  0x0090 : 69 64 65 6F 46 75 6E 63 74 69 6F 6E 00 3F F0 00 : ideoFunction.?..
  0x00A0 : 00 00 00 00 00 00 08 66 6C 61 73 68 56 65 72 02 : .......flashVer.
  0x00B0 : 00 0E 57 49 4E 20 31 30 2C 30 2C 32 32 2C 38 37 : ..WIN.10,0,22,87
  0x00C0 : 00 04 66 70 61 64 00 00 00 00 00 00 00 00 00 00 : ..fpad..........
  0x00D0 : 0C 63 61 70 61 62 69 6C 69 74 69 65 73 00 40 2E : .capabilities.@.
  0x00E0 : 00 00 00 00 00 00 00 0E 6F 62 6A 65 63 74 45 6E : ........objectEn
  0x00F0 : 63 6F 64 69 6E 67 00 00 00 00 00 00 00 00 00 00 : coding..........
  0x0100 : 00 09                                           : ..
 
[debug] got packet from server. type: 'packet_server_bw' 
[debug] 
  0x0000 : 07 A1 20 00                                     : ....
 
[debug] got packet from server. type: 'packet_client_bw' 
[debug] 
  0x0000 : 00 03 D0 90 02                                  : .....
 
[debug] got packet from server. type: 'packet_server_bw' 
[debug] 
  0x0000 : 00 03 D0 90                                     : ....
 
[debug] got packet from server. type: 'packet_invoke' 
[debug] 
  0x0000 : 02 00 07 5F 72 65 73 75 6C 74 00 3F F0 00 00 00 : ..._result.?....
  0x0010 : 00 00 00 05 03 00 05 6C 65 76 65 6C 02 00 06 73 : .......level...s
  0x0020 : 74 61 74 75 73 00 04 63 6F 64 65 02 00 1D 4E 65 : tatus..code...Ne
  0x0030 : 74 43 6F 6E 6E 65 63 74 69 6F 6E 2E 43 6F 6E 6E : tConnection.Conn
  0x0040 : 65 63 74 2E 53 75 63 63 65 73 73 00 0B 64 65 73 : ect.Success..des
  0x0050 : 63 72 69 70 74 69 6F 6E 02 00 15 43 6F 6E 6E 65 : cription...Conne
  0x0060 : 63 74 69 6F 6E 20 73 75 63 63 65 65 64 65 64 2E : ction.succeeded.
  0x0070 : 00 00 09                                        : ...
 
[debug] $VAR1 = {
  'args' => [
    undef,
    {
      'level' => 'status',
      'description' => 'Connection succeeded.',
      'code' => 'NetConnection.Connect.Success'
    }
  ],
  'method' => '_result',
  'id' => '1'
};
 
[debug] sending packet. type: 'packet_invoke' method: 'createStream' 
[debug] 
  0x0000 : 03 00 00 00 00 00 18 14 00 00 00 00 02 00 0C 63 : ...............c
  0x0010 : 72 65 61 74 65 53 74 72 65 61 6D 00 40 08 00 00 : reateStream.@...
  0x0020 : 00 00 00 00                                     : ....
 
[debug] got packet from server. type: 'packet_client_bw' 
[debug] 
  0x0000 : 00 03 D0 90 02                                  : .....
 
[debug] got packet from server. type: 'packet_ping' 
[debug] 
  0x0000 : 00 00 00 00 00 00                               : ......
 
[debug] got packet from server. type: 'packet_invoke' 
[debug] 
  0x0000 : 02 00 07 5F 72 65 73 75 6C 74 00 40 08 00 00 00 : ..._result.@....
  0x0010 : 00 00 00 05 00 3F F0 00 00 00 00 00 00          : .....?.......
 
[debug] $VAR1 = {
  'args' => [
    undef,
    '1'
  ],
  'method' => '_result',
  'id' => '3'
};
 
[debug] sending packet. type: 'packet_invoke' method: 'publish' 
[debug] 
  0x0000 : 03 00 00 00 00 00 25 14 01 00 00 00 02 00 07 70 : ......%........p
  0x0010 : 75 62 6C 69 73 68 00 40 10 00 00 00 00 00 00 05 : ublish.@........
  0x0020 : 02 00 0E 74 65 73 74 31 32 33 38 33 39 37 36 38 : ...test123839768
  0x0030 : 35                                              : 5
 
[debug] got packet from server. type: 'packet_ping' 
[debug] 
  0x0000 : 00 00 00 00 00 01                               : ......
 
[debug] got packet from server. type: 'packet_invoke' 
[debug] 
  0x0000 : 02 00 08 6F 6E 53 74 61 74 75 73 00 40 10 00 00 : ...onStatus.@...
  0x0010 : 00 00 00 00 05 03 00 05 6C 65 76 65 6C 02 00 06 : ........level...
  0x0020 : 73 74 61 74 75 73 00 04 63 6F 64 65 02 00 17 4E : status..code...N
  0x0030 : 65 74 53 74 72 65 61 6D 2E 50 75 62 6C 69 73 68 : etStream.Publish
  0x0040 : 2E 53 74 61 72 74 00 0B 64 65 73 63 72 69 70 74 : .Start..descript
  0x0050 : 69 6F 6E 02 00 1A 50 75 62 6C 69 73 68 69 6E 67 : ion...Publishing
  0x0060 : 20 74 65 73 74 31 32 33 38 33 39 37 36 38 35 2E : .test1238397685.
  0x0070 : 00 08 63 6C 69 65 6E 74 69 64 00 41 A1 36 D4 30 : ..clientid.A.6.0
 
[debug] got packet from server. type: 'packet_invoke' 
[debug] 
  0x0000 : 02 00 08 6F 6E 53 74 61 74 75 73 00 40 10 00 00 : ...onStatus.@...
  0x0010 : 00 00 00 00 05 03 00 05 6C 65 76 65 6C 02 00 06 : ........level...
  0x0020 : 73 74 61 74 75 73 00 04 63 6F 64 65 02 00 17 4E : status..code...N
  0x0030 : 65 74 53 74 72 65 61 6D 2E 50 75 62 6C 69 73 68 : etStream.Publish
  0x0040 : 2E 53 74 61 72 74 00 0B 64 65 73 63 72 69 70 74 : .Start..descript
  0x0050 : 69 6F 6E 02 00 1A 50 75 62 6C 69 73 68 69 6E 67 : ion...Publishing
  0x0060 : 20 74 65 73 74 31 32 33 38 33 39 37 36 38 35 2E : .test1238397685.
  0x0070 : 00 08 63 6C 69 65 6E 74 69 64 00 41 A1 36 D4 30 : ..clientid.A.6.0
  0x0080 : 00 00 00 00 00 09                               : ......
 
[debug] $VAR1 = {
  'args' => [
    undef,
    {
      'clientid' => '144402968',
      'level' => 'status',
      'description' => 'Publishing test1238397685.',
      'code' => 'NetStream.Publish.Start'
    }
  ],
  'method' => 'onStatus',
  'id' => '4'
};
 
[debug] callback 'on_invoke_onStatus' 
[debug] onStatus.code=NetStream.Publish.Start 
[info] publish started successfuly. 
[debug] sending packet. type: 'packet_invoke' method: 'closeStream' 
[debug] 
  0x0000 : 03 00 00 00 00 00 17 14 00 00 00 00 02 00 0B 63 : ...............c
  0x0010 : 6C 6F 73 65 53 74 72 65 61 6D 00 40 14 00 00 00 : loseStream.@....
  0x0020 : 00 00 00                                        : ...