Mojolicious-Plugin-PNGCast/lib/Mojolicious/Plugin/PNGCast.pm
package Mojolicious::Plugin::PNGCast;
use strict;
use 5.014;
use Mojo::Base 'Mojolicious::Plugin';
use Filter::signatures;
use feature 'signatures';
no warnings 'experimental::signatures';
use Future::Mojo;
our $VERSION = '0.22';
=head1 NAME
Mojolicious::Plugin::PNGCast - in-process server to display a screencast
=head1 DESCRIPTION
Use this web application to display the screencast of a (headless) web browser
or other arbitrary PNG data sent to it via websocket.
The synopsis shows how to use this plugin to display
a Chrome screencast using L<WWW::Mechanize::Chrome>.
=head1 SYNOPSIS
use Mojolicious::Lite;
use Mojo::Server::Daemon;
use WWW::Mechanize::Chrome;
plugin 'PNGCast';
my $daemon_url = 'http://localhost:3000';
my $ws_monitor = Mojo::Server::Daemon->new(app => app());
$ws_monitor->listen([$daemon_url]);
$ws_monitor->start;
my $mech = WWW::Mechanize::Chrome->new( headless => 1 );
$mech->setScreenFrameCallback( sub {
app->send_frame( $_[1]->{data} )}
);
print "Watch progress at $daemon_url\n";
sleep 5;
$mech->get('https://example.com');
=cut
has 'clients' => sub { {} };
has 'last_frame' => undef;
=head1 HELPERS
=head2 C<< app->send_frame >>
app->send_frame( $png_data );
Sends a frame to all connected clients. If a fresh client connects, it will
receive the last frame handed to C<send_frame>.
=head1 INTERNAL METHODS
=head2 C<< $plugin->notify_clients >>
$plugin->notify_clients( $PNGframe )
Notify all connected clients that they should display the new frame.
=cut
sub notify_clients( $self, @frames ) {
my $clients = $self->clients;
for my $client_id (sort keys %$clients ) {
my $client = $clients->{ $client_id };
for my $frame (@frames) {
eval {
$client->send({ binary => $frame });
};
};
};
}
sub register( $self, $app, $config ) {
$app->routes->get('/' => sub {
my( $c ) = @_;
$c->res->headers->content_type('text/html');
$c->res->headers->connection('close');
$c->render('index')
});
$app->routes->websocket( '/ws' => sub {
my( $c ) = @_;
$c->inactivity_timeout(300);
my $client_id = join ":", $c->tx->original_remote_address || $c->tx->remote_address,
$c->tx->remote_port();
$self->clients->{ $client_id } = $c;
$c->tx->on( json => sub {
my( $c, $data ) = @_;
#warn Dumper $data ;
warn "Click received (and ignored)";
#$mech->click( { selector => '//body', single => 1 }, $data->{x}, $data->{y} );
#$mech->click( { selector => '//body', single => 1 }, $data->{x}, $data->{y} );
});
#warn("Client connected");
if( $self->last_frame ) {
# send current frame
$c->send({ binary => $self->last_frame });
} else {
# send a standby frame ??
};
$c->tx->on( finish => sub {
my( $c,$code,$reason ) = @_;
warn "Client gone ($code,$reason)" ;
delete $self->clients->{ $client_id };
});
});
# Stop our program
$app->routes->get( '/stop' => sub {
my( $c ) = @_;
$c->res->headers->content_type('text/html');
$c->res->headers->connection('close');
$c->render('stop');
Mojo::IOLoop->stop;
});
$app->helper( 'send_frame' => sub ( $c, $framePNG ) {
# send this frame to all connected clients
if( scalar keys %{ $self->clients } ) {
Future::Mojo->new->done_next_tick( 1 )
->then( sub {
$self->notify_clients( $framePNG );
})->retain;
};
$self->last_frame( $framePNG );
});
# Install our templates
push @{$app->renderer->classes}, __PACKAGE__;
push @{$app->static->classes}, __PACKAGE__;
}
=head1 EXPORTED HTTP ENDPOINTS
This plugin makes the following endpoints available
=over 4
=item *
C</> - the index page
This is an HTML page that opens a websocket to the webserver and listens for
PNG images coming in over that websocket
=item *
C</ws> - the websocket
This is a websocket
=item *
C</stop> - stop the application
This stops the complete Mojolicious application
=back
=head1 REPOSITORY
The public repository of this module is
L<https://github.com/Corion/Mojolicious-Plugin-PNGCast>.
=head1 SUPPORT
The public support forum of this module is L<https://perlmonks.org/>.
=head1 BUG TRACKER
Please report bugs in this module via the RT CPAN bug queue at
L<https://rt.cpan.org/Public/Dist/Display.html?Name=Mojolicious-Plugin-PNGCast>
or via mail to L<Mojolicious-Plugin-PNGCast-Bugs@rt.cpan.org|mailto:Mojolicious-Plugin-PNGCast-Bugs@rt.cpan.org>.
=head1 AUTHOR
Max Maischein C<corion@cpan.org>
=head1 COPYRIGHT (c)
Copyright 2010-2018 by Max Maischein C<corion@cpan.org>.
=head1 LICENSE
This module is released under the same terms as Perl itself.
=cut
1
__DATA__
@@ stop.html.ep
<html><body>Bye</body></html>
@@ index.html.ep
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"/><title>Hessian</title>
<script>
function install() {
var output = document.getElementById('hessianHead');
var status = document.getElementById('status');
var exampleSocket = new WebSocket(location.origin.replace(/^http/, 'ws')+"/ws");
exampleSocket.binaryType = 'arraybuffer';
exampleSocket.onopen = function(evt) {
status.innerHTML = "Connected";
};
exampleSocket.onerror = function(evt) {
status.innerHTML = "Error:" + evt;
};
exampleSocket.onclose = function(evt) {
status.innerHTML = "Closed";
};
exampleSocket.onmessage = function(evt) {
if (evt.data instanceof ArrayBuffer) {
var length = evt.data.byteLength;
var blob = new Blob([evt.data],{type:'image/png'});
var url = URL.createObjectURL(blob);
var image = document.getElementById("hessianHead");
var img = new Image();
img.onload = function(){
var ctx = image.getContext("2d");
ctx.drawImage(img, 0, 0);
}
img.src = url;
}
};
output.onclick = function(evt) {
console.log(evt);
exampleSocket.send(JSON.stringify( { action:"click", x: evt.offsetX, y: evt.offsetY }));
}
status.innerHTML = "Connecting";
};
</script>
</head>
<body onload="javascript:install()">
<canvas id="hessianHead" width="1280" height="800"></canvas>
<div id="status">Javascript required</div><div><a href="/stop">Stop</a></div>
</body></html>