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: