Tatsumaki でニコ生放送のコメントを流してみた

最近 Tatsumaki 使ったアプリ作りたいなぁと考えていて 何かネタはないかと探していました。 chatはサンプルであるしTwitterのtimelineは既に作られてるし・・・

ニコ生見ててひらめきました。そうです放送中に流れるコメントです。 放送番組を指定して番組中に流れるリスナーのコメントが流れる様子をTatsumakiを使って実現してみる事になりました。

コメントだけ見れて誰得?みたいな感じですが・・・あくまでネタなので。。

ニコ生のコメントは番組毎のサーバーをsocketで叩くとchatデータがデレデロ流れてくるのでそいつをTatsumakiのstreamに渡してやれば上手くいきそうです。

でとりあえず実装できたのですが、はまったのがIEブラウザの挙動です。

どういう訳かIEのみ一度表示したデータを永遠と表示してしまうバグ?のようなものがあったり JSONで送られてくるデータをcacheしてしまったりと散々な目にあいました。

デモで使われているスクリプト

スクリプトの主要な部分を載せておきます。

app.psgi

use Tatsumaki::Application;
use strict;
use warnings;
use NicoHandler;
use NicoChatHandler;
use RssHandler;
use File::Basename;

my $app = Tatsumaki::Application->new(
    [
        '/nico/live/(\d+)'   => 'RssHandler',
        '/nico/lv(\d+)'      => 'NicoHandler',
        '/nico_pool/lv(\d+)' => 'NicoChatHandler',
    ]
);

$app->template_path( dirname(__FILE__) . "/template" );
$app->static_path(dirname(__FILE__) . "/static");
return $app;

RssHandler.pm

package RssHandler;

use parent qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
use strict;
use warnings;
use Tatsumaki::HTTPClient;
use XML::Simple qw(XMLin);

sub get {
    my $self   = shift;
    my $query  = shift || 1;
    my $sort   = defined $self->request->param("sort") ? "&sort=view" : "&sort=start";
    my $client = Tatsumaki::HTTPClient->new;
    $client->get(
        "http://NICONAMA/URL?tab=common&p=" 
          . $query
          . $sort,
        $self->async_cb( sub { $self->on_response(@_) } )
    );
}

sub on_response {
    my ( $self, $res ) = @_;
    if ( $res->is_error ) {
        Tatsumaki::Error::HTTP->throw(500);
    }
    else {
        my $rss = XMLin( $res->content );    # warn Dumper $rss;
        $self->render( 'nico_rss.html',
            { rss_item => $rss->{channel}->{item} } );
    }
}

1;

NicoHandler.pm

package NicoHandler;
use parent qw(Tatsumaki::Handler);
use strict;
use warnings;

sub get {
    my $self = shift;
    my ( $uid ) = @_;
    $self->render( 'nico.html' );
}
1;

NicoChatHandler.pm

package NicoChatHandler;

use parent qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
use strict;
use warnings;
use utf8;
use Nico;
use Nico::TatsumakiStream;
use Tatsumaki::MessageQueue;
use Tatsumaki::Error;
use Encode qw(decode_utf8);

my %streams;


sub create_niconico {

    my ( $self, $live_id ) = @_;
    # Get Nico Live Status
    my $niko = Nico->new("/tasumaki_stream/config.yaml");

    $niko->login or die "Not login";
    my $live_info = $niko->live_info( $live_id );
    die "Status Faild" unless $live_info->status;
    my $stream_instance = Nico::TatsumakiStream->instance;

    # NicoNico Stream Session CallBack Return
    return $stream_instance->stream( $live_info  );
}

sub create_stream {
    my $self = shift;
    my ( $uid ) = @_;
    my $cnt ||= 0;

    my $mq = Tatsumaki::MessageQueue->instance( $uid );

    $streams{ $uid} ||= $self->create_niconico( $uid);

    my $cb = sub {
        # warn "Live Chat:", $_[0]->{text};
        $_[0]->{text} =  decode_utf8 $_[0]->{text};
        $_[0]->{text} =~ s/\n/
/g;
        $mq->publish( { type => 'chat', chat => $_[0] } );
    };

    my $err_cb = sub {
        $mq->publish( { type => 'message', text => $_[0], } );
    };

    $streams{ $uid }->($cb, $err_cb);
}

sub get {
    my $self = shift;
    my ( $uid ) = @_;# warn "GET UID:", $uid;

    my $session = $self->request->param('session')
                     or Tatsumaki::Error::HTTP->throw(500, "'session' needed");

    $streams{ $uid } or $self->create_stream( $uid );
    my $mq = Tatsumaki::MessageQueue->instance( $uid );
    $mq->poll_once( $session, 
        sub {
            my @events_published = @_; 
            $self->write( \@events_published );

            $self->response->header(
               'Cache-Control' => 'no-store, no-cache, must-revalidate,'.
                  'post-check=0, pre-check=0, max-age=0',
                'Pragma' => 'no-cache',
                'Expires' => 'Thu, 01 Jan 1970 00:00:00 GMT'
            );

            $self->finish;
        } 
  );
}
1;

TatsumakiStream.pm

package Nico::TatsumakiStream;

use strict;
use warnings;
use base "Class::Singleton";
use Nico::Chat       ();
use AnyEvent         ();
use AnyEvent::Handle ();
use AnyEvent::Socket qw(tcp_connect);
use Encode qw(decode_utf8);
use Nico::Utils qw(instanceof);

our $VERSION = '0.03';
our $DEBUG   = 0;

my $STREAM_POST_DATA =
  qq{\0};

# stream session getter and setter
sub stream {
    my ( $self, $connect_info ) = @_;

    die "stream method ARGS is (Nico::AlertInfo or Nico::Live) Instance only."
      unless ( instanceof( $connect_info => 'Nico::AlertInfo' ) )
      || ( instanceof( $connect_info => 'Nico::Live' ) );

    return $self->{ $connect_info->instance_name } ||=
      __create_stream($connect_info);
}

# New Stream Session Create
sub __create_stream {
    my $connect_info = shift;
    sub {
        my $on_read_cb  = shift || sub { warn $_[0] };
        my $on_error_cb = shift || sub { warn $_[0] };

        my $wsession;
        $wsession ||= tcp_connect $connect_info->addr, $connect_info->port,

          # TCP Connect CallBack
          sub {
            my ($fh) = @_
              or die "unable to connect: $!";

            my $handle;    # avoid direct assignment so on_eof has it in scope.
            $handle = new AnyEvent::Handle
              fh       => $fh,
              on_error => sub {
                $on_error_cb->("Handle Error");
                $_[0]->destroy;
                undef $wsession;
              },
              on_eof => sub {
                $on_error_cb->("/Disconnect");
                $handle->destroy;    # destroy handle
                undef $wsession;
              };

            $handle->push_write( sprintf $STREAM_POST_DATA,
                $connect_info->thread );

            $handle->push_read(
                line => "\0",
                sub {
                    my ( $handle, $line ) = @_;

                    print "HEADER\n$line\n\nBODY\n" if $DEBUG;
                    $handle->on_read(
                        sub {

                            # print response body
                            my $buffer = $_[0]->rbuf;
                            for my $chat (
                                @{ Nico::Chat::chat_line2hash( $buffer, 1 ) } )
                            {
                                $on_read_cb->($chat);

                                if ( $chat->{text} eq '/disconnect' ) {
                                    $on_error_cb->( $chat->{text} );
                                    $handle->destroy;
                                    undef $wsession;
                                }
                            }
                            $_[0]->rbuf = "" if defined $wsession;

                        }
                    );
                }
            );
          }, sub {
            my ($fh) = @_;
            # Also limit the connection timeout to 15 seconds.
            # could call $fh->bind etc. here
            15;
          };
      }
}
1;

nico.html

preタグ使ってるんだが崩れるなぁ

% my $uid = $_[0]->{handler}->args->[0];
<html>
<head>
<title>NicoNico Live  lv<%= $uid %></title>
<script type="text/javascript" src="/static/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="/static/jquery.ev.js"></script>
<script type="text/javascript" src="/static/jquery.dump.js"></script>
<script type="text/javascript">
jQuery( function ($) {
var last=0;
    $.ev.loop('/nico_pool/lv' + <%= $uid %> + '?session=' + Math.random(), {

        chat : function(ev) {
            if(! ev.chat.text) return;
            var kid = ev.chat.no;
            if(last >= kid){return;} /* これが無いとIEで悲惨な目に合います>< */
            $( "#chats" ).prepend( 
                ev.chat.no + ' : ' + ev.chat.text + "<br>" 
            );
            last = kid;
        },
        message : function(ev) {
        $("#last2").html("<div>" +  ev.text + "</div>");
            $( "#chats" ).prepend( 
                '<span style="color: #F00;">' + ev.text + "</span><br>" 
            );
        }
    });
} );
</script>
</head>
<body>
<div id="last" style="font-size:xx-small;"></div>
<div id="last2" style="font-size:xx-small;"></div>
<h1>NicoNico Live  <a href="http://live.nicovideo.jp/watch/lv<%= $uid %>">lv<%= $uid %></a></h1>
<div id="chats" style="font-size:xx-small;"></div>
</body>
</html>
created:

Back to top