Kamaitachi入門(その5)

Kamaitachi の追っかけ記事の第5回目です。

いいかげんねちっこく人のソースを追いかけるのもいかがなものか、と感じつつも、id:ZIGOROuに励ましてもらったので、もう少し続けます。

さて今回はKamaitachi::IOStreamを攻めます。

Kamaitachi::IOStreamとはRTMPストリームのreader/writerです。

主立った役割としてはこんな感じ。

  1. Socketに入ってきたデータはKamaitachi::IOStreamのBufferにpushされる
  2. Bufferに入ったデータはget_packet()により適切なBytes数がread()される
  3. read()では適宜バイナリをunpack
  4. 逆にwrite()ではデータをAMFのカタチにserializeしてからsocketに書き込む

またIO−Bufferのための操作用のメソッド(spinとか)が色々と用意されているのですが、そいつらがなんとなくコジャレています。

またこのクラスはData::AMF::IOをextends しているのも大きな特徴です。
(Data::AMFとはtypesterさんが公開しているCPANモジュールで、perlのデータ構造をAMFバイナリのカタチにシリアライズしたり、その反対にAMFバイナリをperlのデータ構造にデシリアライズしてくれるモジュールです。)


さてさて、このKamaitachi::IOStreamですが、じつはキモはget_packet()というメソッドにあります。

ここにはなんとRTMPパケット解析そのものが実装されているので、要チェックです。

sub get_packet {
    my ($self) = @_;
    my $bref;

    my $chunk_size  = $self->chunk_size;
    my $packet_list = $self->packets;

    #まず先頭1バイトを取得
    $bref = $self->read_u8 or return $self->reset;
    my $first = $$bref;

    # 先頭1バイトの上位2bitが$header_size
    # のこり6bitが$amf_number(パケットの中に格納されているAMFオブジェクトの識別IDみたいなもの)
    my $header_size = $first >> 6;
    my $amf_number  = $first & 0x3f;

    # ここの部分、何をしているのかちょっと不明
    # $amf_numberが0もしくは1って何か特別な意味があるのかな?
    if ($amf_number == 0) {
        $bref = $self->read_u8 or return $self->reset;
        $amf_number = $$bref;
    }
    elsif ($amf_number == 1) {
        $bref = $self->read_u16 or return $self->reset;
        $amf_number = $$bref;
    }

    # すでに扱っている$amf_numberであれば$packet_listからパケットを取得
    # 初めての$amf_numberであればKamaitachi::Packetでnewする
    my $packet = $packet_list->[ $amf_number ] || Kamaitachi::Packet->new( socket => $self->socket, number => $amf_number );

    ##ここからちょっとトリッキー
    ## 実際のヘッダ長は以下のようにマッピングされる
    ##   00 = 12bytes
    ##   01 =  8bytes
    ##   10 =  4bytes
    ##   11 =  1bytes

    # $header_sizeが2以下ということは10以下、つまり
    # 12bytes, 8bytes, 4bytesの場合がここの条件にあてはまる
    if ($header_size <= 2) {
        if ($header_size == 2 and !$packet->size) { # XXX
            warn 'skip packet';
            $self->clear;
            return;
        }
        # 3bytes分を取得
        # この3bytesはタイムスタンプを意味する
        # なおこの時点で$paket->partialに1を代入
    #(分割されているパケットの可能性あり、という意味かな?)
        $bref = $self->read_u24 or return $self->reset;
        $packet->timer( $$bref );
        $packet->partial(1);
    }

    # $header_sizeが1以下ということは01以下、つまり
    # 12bytes, 8bytesの場合がここの条件にあてはまる
    if ($header_size <= 1) {
        # 次の3bytesを取得
        # ボディ長を表す
        $bref = $self->read_u24 or return $self->reset;
        if ($$bref >= 100000) { # XXX: might be invalid packet...
            warn 'skip packet, invalid size:' . $$bref;
            $self->clear;
            return;
        }
        # sizeとしてボディ長を格納 
        $packet->size( $$bref );

        # 次の1byteはパケットタイプ
        $bref = $self->read_u8 or return $self->reset;
        $packet->type( $$bref );

        # dataとかrawを初期化
        # なおpartialに0をセット
        #(ヘッダが8bytes以上であれば、このパケットは分割されていないよ、という意味?)
        $packet->data(q[]);
        $packet->raw(q[]);
        $packet->partial(0);
    }

    # $header_sizeが0以下というのは00、つまり
    # 12bytesの場合のみ、ここの条件にはまる
    if ($header_size <= 0) {
        # 4bytesを取得
        # AMFオブジェクトのストリームIDを意味する
        $bref = $self->read_u32 or return $self->reset;
        $packet->obj( $$bref );
    }

    my $data = q[];
    my $size = $packet->size;

    # ここは何をしているのかイマイチよくわからない
    if ($packet->data and bytes::length($packet->data) < $size) {
        $data = $packet->data;
        $size -= bytes::length($packet->data);
    }

    # データのリード部
    if ($size > 0) {
        #実際に取得したいデータサイズ決定(デフォルトは128bytes)
        my $want = $size <= $chunk_size ? $size : $chunk_size;

        # データを欲しい分だけリード
        $bref = $self->read($want) or return $self->reset;

        # 変数に格納
        $packet->partial_data( $$bref );
        $data .= $packet->partial_data;
    }

    # 色々データを整理
    $packet->data($data);
    $packet->{raw} = $self->spin;

    # パケット格納用リストにこのパケットを格納しておく
    $packet_list->[ $amf_number ] = $packet;

    # パケットをかえす。おつかれ〜。
    $packet;
}

うーん、むずかしいですね。
RTMPパケットの構造が解っていないと何をしているのかさっぱりわからないです。

要所要所で適当なコメントをつけてみましたが、正直あっているのか自信ありません。
何カ所か理解できない部分もありますし、間違っている箇所があればご指摘ください( typester さま)

なおRTMPパケットについては前々回のエントリでくわしく書いておいたので知りたい人はそっちを参照してみてください。