use Mojolicious::Lite;

app->attr(cam1 => sub {
  open my $fh, qq(ffmpeg  -i 'rtsp://192.168.128.10:554/user=admin&password=&channel=1&stream=0.sdp?' -f mpegts -codec:v mpeg1video  -b:v 1200k -an  - 2>/dev/null |)
    or die "cannot run ffmpeg: $!"; # -s 1280x720  -bf 0 
  binmode($fh);
  my $stream = Mojo::IOLoop::Stream->new($fh);
  $stream->start;
  return $stream;
});

app->attr(feeds => sub {
  my $app = shift;
  my $cam1 = $app->cam1;
  
  my $feeds = {'/feed/cam1'=>{tx=>$cam1, clients=>{}}};
  
  $cam1->on(read => sub {
    my ($stream, $bytes) = @_;
      $_->tx->send({binary => $bytes})
          for (values %{$feeds->{'/feed/cam1'}{clients}});
  });
  return $feeds;
});

get '/feed/cam1' => 'index';

websocket '/feed' => sub {
  my $ws = shift;
  #~ $ws->inactivity_timeout(300);# переподключение вебсокета
  my $feed = $ws->param('feed')
    or return $ws->send("none param feed?");
  
  $ws->log->info("Client connect try feed=$feed, but feeds=[@{[ keys %{ $ws->app->feeds} ]}]");
  $feed = $ws->app->feeds->{$feed}
    or return $ws->send("none yet feed ".$ws->param('feed'));
  
  $feed->{clients}{"$ws"} = $ws;
  $ws->tx->with_protocols('binary');
  $ws->log->info("Client connected: $ws; to feed: ".$feed->{tx}."; clients: @{[ scalar keys %{ $feed->{clients} } ]}");
  
  $ws->on(
    finish => sub {
      my( $ws, $code, $reason ) = @_ ;
      delete $feed->{clients}{ "$ws" };
      $ws->log->info("Client disconnected: $ws; clients: @{[ scalar keys %{ $feed->{clients} } ]}");
    }
  );

};

app->start;

__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
  <title>Camero #1</title>
</head>
<body>

  <h1>Camero #1</h1>

  <canvas></canvas>
  <script type="text/javascript" src="//jsmpeg.com/jsmpeg.min.js"></script>
  <script type="text/javascript">
    var canvas = document.getElementsByTagName('canvas')[0];
    var player = new JSMpeg.Player('<%= url_for('feed')->query('feed'=> $c->req->url->path->to_route)->to_abs %>', {canvas: canvas, autoplay: true,});
  </script>

</body>
</html>