#!/usr/bin/perl

#flexTPS - flexable TelePresence System
#Copyright (c) 2003-2011  Codaxus, LLC
#
#Visit: http://flextps.org
#
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA02111-1307USA

#-------------------------------------------------------------------------------

use strict;
use warnings;

use CGI qw(param);

# http://perldoc.perl.org/CGI/Carp.html
# Use of carpout() is not great for performance, so it is recommended for
# debugging purposes or for moderate-use applications.
#use CGI::Carp qw(fatalsToBrowser);

use IO::Socket;
use IO::Select;
use POSIX qw(:errno_h strftime BUFSIZ);

use APR::Socket ();
use APR::Const -compile => qw(SO_NONBLOCK);

use Time::HiRes qw(usleep time);

#use Image::Epeg qw( :constants );

use FlexTPS;

use constant STREAM_DISABLED_JPEG => FlexTPS::BASE_DIR . '/www/images/stream_disabled.jpg';
use constant CONNECTING_TO_STREAM_JPEG => FlexTPS::BASE_DIR . '/www/images/connecting_to_stream.jpg';
use constant DISCONNECTED_FROM_STREAM_JPEG => FlexTPS::BASE_DIR . '/www/images/disconnected_from_stream.jpg';
use constant STREAM_FAILURE_JPEG => FlexTPS::BASE_DIR . '/www/images/stream_failure.jpg';

use constant MAX_RETRIES => 5;
use constant STREAM_TIMEOUT => 10;
use constant JPEG_BOUNDARY => "--------JPEG_FRAME_BOUNDARY";

local $| = 1;

$CGI::POST_MAX=1024;
$CGI::DISABLE_UPLOADS = 1;

main();
exit;

#-------------------------------------------------------------------------------

sub main {
	my $feed_id = param('feed');
	my $stream_id = param('stream');
	my $fps = param('fps');
	my $width = param('width');
	my $height = param('height');
	my $status_frame = param('status_frame');
	my $write_buffer = '';
	my $text = '';
	my $result = 1;

	my $r = Apache2::RequestUtil->request();

	if ( defined($feed_id) && defined($stream_id) && defined($fps) ){
		my $stream = FlexTPS::get_stream(FlexTPS::get_unified_group($r->user), $feed_id, $stream_id);
		my $proxy = FlexTPS::get_proxy($stream->{'proxy-id'});

		if ( defined($stream) && defined($proxy) ){
			$stream->{'proxy-ip'} = $proxy->{'ip'};
			$stream->{'proxy-port'} = $proxy->{'port'};


			if ( defined($width) && $width !~ m/^\d+$/ ){
				undef $width;
			}
			if ( defined($height) && $height !~ m/^\d+$/ ){
				undef $height;
			}

			if ( defined($width) && defined($height) ){
				if (	(defined($stream->{'frame-width'}) &&
					($width > $stream->{'frame-width'})) ||
					(defined($stream->{'frame-height'}) &&
					($height > $stream->{'frame-height'})) ){
					$width = $stream->{'frame-width'};
					$height = $stream->{'frame-height'};
				}
			} elsif ( defined($width) ){
				if ( defined($stream->{'frame-width'}) && ($width > $stream->{'frame-width'}) ){
					$width = $stream->{'frame-width'};
				}
			} elsif ( defined($height) ){
				if ( defined($stream->{'frame-height'}) && ($height > $stream->{'frame-height'}) ){
					$height = $stream->{'frame-height'};
				}
			} else {
				$width = $stream->{'frame-width'};
				$height = $stream->{'frame-height'};
			}

			if ( ($fps > 0) && (($stream->{'stream-enabled'} eq "true") || ($stream->{'stream-enabled'} eq "1")) ){
				$stream->{'feed-id'} = $feed_id;

				if ( $fps < $stream->{'max-fps'} ){
					$stream->{'fps'} = $fps;
				} else {
					$stream->{'fps'} = $stream->{'max-fps'};
				}

				if ( defined($status_frame) && ("$status_frame" eq "true") ){
					stream_frames($stream, $r->user, $r, $width, $height, 1);
				} else {
					stream_frames($stream, $r->user, $r, $width, $height, 0);
				}
			} else {
				my $jpeg;
				my $status;
				if ( ($stream->{'stream-enabled'} ne "true") && ($stream->{'stream-enabled'} ne "1") ){
					$jpeg = FlexTPS::slurp(STREAM_DISABLED_JPEG);
					$status = "disabled";
				} else {
					$jpeg = FlexTPS::slurp(DISCONNECTED_FROM_STREAM_JPEG);
					$status = "disconnected";	
				}

#				$jpeg = resize_jpeg($jpeg, $width, $height);

				my $rfc1123_date = strftime("%a, %e %b %Y %H:%M:%S GMT", gmtime());
				my $len = length($jpeg);
				$write_buffer = "$ENV{'SERVER_PROTOCOL'} 200 OK\r\n" . 
					"Pragma: no-cache\r\n" .
					"Cache-Control: must-revalidate, no-cache, no-store\r\n" .
					"Date: $rfc1123_date\r\n" .
					"Expires: $rfc1123_date\r\n" .
					"Content-Type: image/jpeg\r\n" .
					"Content-Length: " . length($jpeg) . "\r\n" .
					"X-Resource-Status: $status\r\n" .
					"\r\n" . 
					$jpeg . "\r\n";
				send_entire_buffer($r, $write_buffer);
			}
		} else {
			$text = "Invalid feed and stream (feed=$feed_id, stream=$stream_id).";
			$result = 0;
		}
	} else {
		if ( ! defined($feed_id) ){
			$text = "Feed ID (feed=<id>) not supplied.\n";
		} elsif ( ! defined($stream_id) ){
			$text = "Stream ID (stream=<id>) not supplied.\n";
		} elsif ( ! defined($fps) ){
			$text = "FPS (fps=<int>) not supplied.\n";
		}
		$result = 0;
	}

	if ( $result == 0 ){
		my $rfc1123_date = strftime("%a, %e %b %Y %H:%M:%S GMT", gmtime());
		$write_buffer = "$ENV{'SERVER_PROTOCOL'} 200 OK\r\n" .
			"Pragma: no-cache\r\n" .
			"Cache-Control: must-revalidate, no-cache, no-store\r\n" . 
			"Date: $rfc1123_date\r\n" .
			"Expires: $rfc1123_date\r\n" .
			"Content-Type: text/plain\r\n" .
			"Content-Length: " . length($text) . "\r\n" .
			"X-Resource-Status: invalid\r\n" .
			"\r\n" . 
			"$text";
		send_entire_buffer($r, $write_buffer);
	}

	return $result;
}


sub stream_frames {
	my ($stream, $username, $r, $width, $height, $status_frame) = @_;
	my $write_buffer = '';
	my $retries = 0;
	my $error_string = '';
	my $frame_index = 0;
	my $stream_video = 1;
	my $sock;
	my $start_time;
	my $current_time;
	my $check_status_time;
	my $check_status_delay = 2;
	my $rfc1123_date = strftime("%a, %e %b %Y %H:%M:%S GMT", gmtime());

	my $jpeg_boundary = JPEG_BOUNDARY;
	my $boundary_length = length($jpeg_boundary);

	my $usleep_time = ((1/$stream->{'fps'})*1000000)/2;

	$write_buffer =	"$ENV{'SERVER_PROTOCOL'} 200 OK\r\n" . 
	  	"Pragma: no-cache\r\n" .
		"Cache-Control: must-revalidate, no-cache, no-store\r\n" .
		"Date: $rfc1123_date\r\n" .
		"Expires: $rfc1123_date\r\n" .
		"Connection: close\r\n" .
		"Content-Type: multipart/x-mixed-replace; boundary=$jpeg_boundary\r\n" .
		"X-Resource-Status: active\r\n" .
		"\r\n";
	if (send_entire_buffer($r, $write_buffer) ){
		$write_buffer = '';
	} else {
		exit;
	}

	if ( $status_frame == 1 ){
		my $jpeg = FlexTPS::slurp(CONNECTING_TO_STREAM_JPEG);	
#		$jpeg = resize_jpeg($jpeg, $width, $height);

		$write_buffer =  "$jpeg_boundary\r\n" .
			"Content-Type: image/jpeg\r\n" . 
			"Content-Length: " . length($jpeg) . "\r\n" .
			"X-Resource-Status: active\r\n" .
			"\r\n" .
			$jpeg . "\r\n";
		if (send_entire_buffer($r, $write_buffer) ){
			$write_buffer = '';
		} else {
			exit;
		}
	}

	$start_time = time();
	$check_status_time = $start_time + $check_status_delay;
	while ((! $r->connection->aborted()) && ($stream_video == 1)){

		$sock = new IO::Socket::INET (
	                                 PeerAddr => $stream->{'proxy-ip'},
	                                 PeerPort => $stream->{'proxy-port'},
	                                 Proto => 'tcp',
					 Blocking => 0,
	                                );
	
		if ( $sock ){
			my $select = new IO::Select();
			my $read_buffer = "";
			my $proxy_connection_timeout = time + STREAM_TIMEOUT;
	
			autoflush $sock 1;
			print $sock "fps=" . $stream->{'fps'} . "\n";

			$select->add($sock);

			while( (! $r->connection->aborted()) && ($stream_video == 1) ){
				my @ready = $select->can_read(1);
				my @frames;

				if ( @ready > 0 ){
					if ( $sock->read(my $data, POSIX::BUFSIZ*10) ){
						$read_buffer = $read_buffer . $data;
						$data = '';
						@frames = split(/$jpeg_boundary\r\n/, $read_buffer);

						if ( substr($read_buffer, -($boundary_length + 2), $boundary_length) ne $jpeg_boundary){
							$read_buffer = pop(@frames);
						} else {
							$read_buffer = '';
						}
					} else {
						last; 
					}
				}

				foreach my $frame (@frames){
					$/ = "\r\n";
					while(chomp($frame)) {};
					$/ = "\n";

					if ( length($write_buffer) > 0 ){
						$write_buffer = send_buffer($r, $write_buffer);
						if ( ! defined($write_buffer) ){
							$stream_video = 0;
                                                        last;
						}
					}

					if ( $stream_video != 0 && length($write_buffer) == 0 && length($frame) ){
						my ($header, $jpeg) = split(/\r\n\r\n/, $frame, 2);
						$header =~ m/frame_timestamp=([^\r\n]+)/;
						my $timestamp = $1;
						$header =~ m/frame_epoc_hires=([^\r\n]+)/;
						my $epoc_hires = $1;
#						$header =~ m/frame_width=([^\r\n]+)/;
#						my $frame_width = $1;
#						$header =~ m/frame_height=([^\r\n]+)/;
#						my $frame_height = $1;

#						$jpeg = resize_jpeg($jpeg, $width, $height);

						$write_buffer = "$jpeg_boundary\r\n" .
							"Content-Type: image/jpeg\r\n" .
							"Content-Length: " . length($jpeg) . "\r\n";
						if ( $epoc_hires ){
							$write_buffer .= "X-Frame-Epoc-HiRes: $epoc_hires\r\n";
						}
						if ( $timestamp ){
							$write_buffer .= "X-Frame-Timestamp: $timestamp\r\n";
						}
#						if ( $frame_width && $frame_height ){
#							$write_buffer .= "X-Frame-Width: $frame_width\r\n";
#							$write_buffer .= "X-Frame-Height: $frame_height\r\n";
#						}
						$write_buffer .= "X-Resource-Status: active\r\n";

						$write_buffer .= "\r\n" . $jpeg . "\r\n";

						$write_buffer = send_buffer($r, $write_buffer);
						if ( ! defined($write_buffer) ){
							$stream_video = 0;
							last;
						}

						$frame_index = $frame_index + 1;
						$proxy_connection_timeout = time + STREAM_TIMEOUT;
					}
				}

				$current_time = time();

				if (	defined($stream->{'max-connection-length'}) &&
					($stream->{'max-connection-length'} > 0) &&
					($current_time - $start_time > $stream->{'max-connection-length'}) ){

					$stream_video = -2;
				}

				if ( time > $proxy_connection_timeout ){
					$stream_video = -3;
				}

				if ( $stream_video == 1 ){
					if ( $current_time > $check_status_time == 0 ){
						my $stream = FlexTPS::get_stream(	FlexTPS::get_unified_group($username),
												$stream->{'feed_id'}, $stream->{'id'});

						if (    defined($stream) &&
							($stream->{'stream-enabled'} ne "true") &&
							($stream->{'stream-enabled'} ne "1") ){

							$stream_video = -1;
						}
						$check_status_time = $current_time + $check_status_delay;
					}
					#FIX THIS: we should probably have a max timeout here...
					#If the proxy does not return a jpeg in X sec, exit?
				}
			}
		} else {
			$error_string = "[" .  strftime("%a %b %d %T %Y", localtime()) . "] [error] Could not create socket to proxy (" . $stream->{'proxy-ip'} . ":" . $stream->{'proxy-port'} . "): $!\n";
			++$retries;
		}
	
		if (	defined($stream->{'max-connection-length'}) &&
			($stream->{'max-connection-length'} > 0) &&
			(time() - $start_time > $stream->{'max-connection-length'}) ){

			$stream_video = -2;
		}

		if ( $retries >= MAX_RETRIES ){
			$stream_video = -3;
		}

		if ( $stream_video == 1){
			sleep 1;
		}

	}

	if (defined($sock)) {
		close($sock);
	}
	
	if ( $stream_video < 0 ){
		if ( $status_frame == 1 ){
			my $jpeg;
			my $status;
			if ( $stream_video == -1 ){
				$jpeg = FlexTPS::slurp(STREAM_DISABLED_JPEG);
				$status = "disabled";
			} elsif ( $stream_video == -2) {
				$jpeg = FlexTPS::slurp(DISCONNECTED_FROM_STREAM_JPEG);
				$status = "disconnected";
			} else {
				$jpeg = FlexTPS::slurp(STREAM_FAILURE_JPEG);
				$status = "failure";
			}

#			$jpeg = resize_jpeg($jpeg, $width, $height);

			$write_buffer .= "$jpeg_boundary\r\n" .
				"Content-Type: image/jpeg\r\n" .
				"Content-Length: " .  length($jpeg) . "\r\n" .
				"X-Resource-Status: $status\r\n" .
				"\r\n" .
				$jpeg . "\r\n" .
				"$jpeg_boundary--\r\n";
			send_entire_buffer($r, $write_buffer);
		} else {
			$write_buffer .= "$jpeg_boundary--\r\n";
			send_entire_buffer($r, $write_buffer);
		}
	}

	if ( "$error_string" ne '' ){
		print STDERR $error_string;
	}
}

sub send_entire_buffer {
	my ($r, $buffer) = @_;
	my $error = 0;

	if ( defined($r) && defined($buffer) ){
		my $buffer_length = length($buffer);
		while ( $buffer_length > 0 && (! $r->connection->aborted()) && (!$error) ){
			$buffer = send_buffer($r, $buffer);
			if ( defined($buffer) ){
				$buffer_length = length($buffer);
			} else {
				$error = 1;
			}
		}

		if ( $buffer_length > 0 ){
			$error = 1;
		}
	}

	return !$error;
}

sub send_buffer {
	my ($r, $buffer) = @_;

	if ( defined($r) && defined($buffer) ){
		my $buffer_length = length($buffer);
		my $bytes_sent = eval{ $r->print($buffer) };

		if ($@) {
			$buffer = undef;
		}


		if (defined($bytes_sent) ){
			if ($bytes_sent < $buffer_length) {
				$buffer = substr($buffer, $bytes_sent);
			} else {
				$buffer = '';
			}
		} else {
			$buffer = undef;
		}
	} else {
		$buffer = undef;
	}

	return $buffer;
}

#NOTE: This was taken out back in the day since it is a resize per frame per client.
#      At one point the flexTPS portal allowed a user/client to as for a stream with
#      a user defined scaling size, but it was determined that this had the risk of
#      creating too large of a load on the web server if there were lots of clients
#      so it was commented out and stream resizing was relegated to the admin and the
#      proxy level (resize per frame rather than resize per frame per client). The
#      commented out code was left in the event someone REALLY wants to turn it back
#      on.
#sub resize_jpeg{
#	my ($jpeg, $width, $height) = @_;
#
#	if ( defined($width) || defined($height) ){
#		my $img = Image::Epeg->new(\$jpeg);
#		my $incoming_width = $img->get_width();
#		my $incoming_height = $img->get_height();
#
#		if ( defined($width) && defined($height) ){
#			$img->resize($width, $height, IGNORE_ASPECT_RATIO);
#		} elsif ( defined($width) ) {
#			$height = int($incoming_height * $width/$incoming_width);
#			$img->resize($width, $height, MAINTAIN_ASPECT_RATIO);
#		} elsif ( defined($height) ){
#			$width = int($incoming_width * $height/$incoming_height);
#			$img->resize($width, $height, MAINTAIN_ASPECT_RATIO);
#		}
#		$jpeg = $img->get_data();
#	}
#
#	return $jpeg;
#}

