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: