Difference between revisions of "How to use a video file as a Source"

From ZoneMinder Wiki
Jump to navigationJump to search
(Created page with "There was [https://forums.zoneminder.com/viewtopic.php?f=43&t=31176 a question on the forums] a while ago asking whether it was possible to test zones against stored events, w...")
 
(New code to allow the use of Event IDs; better Testing and Usage instructions; general tidying up)
 
Line 1: Line 1:
There was [https://forums.zoneminder.com/viewtopic.php?f=43&t=31176 a question on the forums] a while ago asking whether it was possible to test zones against stored events, which interested me, and I've cobbled together a fairly reliable method of doing it with PHP and FFMpeg.
There was [https://forums.zoneminder.com/viewtopic.php?f=43&t=31176 a question on the forums] a while ago asking whether it was possible to test zones against stored events, which interested me, and I've cobbled together a method of doing it with PHP and FFMpeg. You can use it to stream a ZM event or a video file.
 
=== How it works ===
 
FFmpeg does all the real work, reading a video file at real-time speed and outputting it to the client. The rest of the code is a wrapper around the FFmpeg command, validating parameters and locating the file to be streamed.


=== Code ===
=== Code ===


Create a file named <kbd>replay-test.php</kbd> in your server's web root (not the ZM root), paste this into it, fixing the source video and ffmpeg paths to suit your installation, and save it. On CentOS the web root is <kbd>/var/www/html</kbd> so you'd do something like, <kbd>sudo nano /var/www/html/replay-test.php</kbd>
Create a file named <kbd>replay-test.php</kbd> in your server's web root (not the ZM root) and paste this into it. You shouldn't have to change anything, just copy/paste/save.


<div style="height: 300px; overflow: scroll;">
<pre>
<pre>
<?PHP
<?PHP


// Change this to point to your source video file
// Usage / ZM Source syntax
$infile = "/home/zoneminder/events/1/2021-11-30/173016/173016-video.mp4";
// To stream a ZM event: http://server/replay-test.php?e=eventID
// To stream a video file: http://server/replay-test.php?f=/full/path/to/file.mp4
 
// Source file
// Used only if you don't set an eventID below and don't
// pass a filename or event ID in the URL query string.
// Must be the /full/path/to/file.mp4
$infile = "";
 
// Event ID
// Used only if you don't pass an eventID or filename in the URL query string.
// Use 0 for none
$eventID = 0;
 
// Database info
// We need this when using event IDs.
// We'll try to get to get it from the
// ZM conf files so you'll only need to
// set it if that fails and you get an error.
// If you set one item then you must set all.
$db = array(    "ZM_DB_HOST" => "",
"ZM_DB_NAME" => "",
"ZM_DB_USER" => "",
"ZM_DB_PASS" => "" );


// Whether to loop the video
// Whether to loop the video
// Use -1 for infinite,
// Use -1 for infinite,
// 0 for single-shot,
// 0 for single-shot,
// 1 for 1 loop (two 'plays'), etc.
// 1 for 1 loop (2 plays), etc.
// Anything other than -1 will throw errors in the ZM log as
// Anything other than -1 will throw warnings in the ZM
// it fails to capture and restarts the zmc process
// log when the stream ends and the zmc_ process restarts.
$loopcount = -1;
$loopcount = -1;


Line 22: Line 50:
$ffmpeg = "/usr/bin/ffmpeg";
$ffmpeg = "/usr/bin/ffmpeg";


// No variables defined below here
// Path to grep
$grep = "/bin/grep";


// Got a filename as a parameter?
// No configuration variables below here
 
// URL parameters
if( !empty($_GET['f']) ) {
if( !empty($_GET['f']) ) {
$infile = $_GET['f'];
$infile = $_GET['f'];
$eventID = "";
}
 
if( !empty($_GET['e']) ) {
$eventID = $_GET['e'];
}
 
// Got anything to do?
if( empty($infile) && empty($eventID) ) {
header("HTTP/1.1 400 Bad Request (no filename or event ID)");
echo "No filename or event ID";
exit;
}
 
// Get a filepath if we've got an eventID
if( !empty($eventID) ) {
$infile = getFilenameByEventID($eventID);
}
}


// Can we read the file?
// Can we read the file?
if( !is_readable($infile) ) {
if( empty($infile) || !is_readable($infile) ) {
header("HTTP/1.1 500 Internal Server Error (can't read $f)");
header("HTTP/1.1 500 Internal Server Error (can't read file)");
exit;
echo "Can't read file '".$infile."')";
exit;
}
 
// ffmpeg good to go?
if( !is_executable($ffmpeg) ) {
header("HTTP/1.1 500 Internal server error (can't execute ffmpeg)");
echo "Can't execute ffmpeg - please set the path";
exit;
}
}


// Don't allow max_execution_time to kill us
// Don't let max_execution_time kill us
set_time_limit(0);
set_time_limit(0);
// No output buffering or compression
// Try to cover all the bases...
@apache_setenv('no-gzip', 1);
@ini_set('output_buffering', 'Off');
@ini_set('output_handler', '');
@ini_set('zlib.output_compression', 'Off');
@ob_end_flush();
@flush;


// Tell the client what we're sending
// Tell the client what we're sending
header( "Content-Type: video/mp4" );
header( "Content-Type: video/mp4" );


// Send it...
// And invoke FFmpeg to send it...
passthru( $ffmpeg." -stream_loop ".$loopcount." -re -i '".$infile."' -vcodec copy -an -vsync 0 -f mp4 -movflags frag_keyframe+empty_moov - 2>/dev/null");
passthru( $ffmpeg." -stream_loop ".$loopcount." -re -fflags +genpts -i '".$infile."' -vcodec copy -an -sn -vsync 0 -map_metadata -1 -f mp4 -movflags frag_keyframe+empty_moov - 2>/dev/null");
 
// All done


?>
exit;
</pre>


=== Testing ===
function getFilenameByEventID( $eventID ) {
// We'll need these
// Don't like globals but don't want to pass anything other than eventID
$db = $GLOBALS['db'];
$grep = $GLOBALS['grep'];


You can test the stream by opening it in a browser or media player such as MPC-HC or VLC: <kbd><nowiki>http://ip.add.re.ss/replay-test.php</nowiki></kbd>
// Do we need to get database details from the .conf?
if( empty($db['ZM_DB_HOST']) ) {
if( !is_executable($grep) ) {
header("HTTP/1.1 500 Internal server error (can't execute grep)");
echo "Can't execute grep - please set the path";
exit;
}
exec("$grep -hE \"^\s*ZM_DB_\" /etc/zm/zm.conf /etc/zm/conf.d/*.conf", $db);
foreach($db as $key => $value) {
unset($db[$key]);
$newkey = trim(preg_replace("/^\s*(ZM_DB_[^=]*)=\s*(.*)\s*$/", "$1", $value ));
$newvalue = trim(preg_replace("/^\s*(ZM_DB_[^=]*)=\s*(.*)\s*$/", "$2", $value ));
if($newkey) {
$db[$newkey] = $newvalue;
}
}
unset($value);
}
 
// Got 'em all?
if( empty($db['ZM_DB_HOST']) || empty($db['ZM_DB_NAME']) || empty($db['ZM_DB_USER']) || empty($db['ZM_DB_PASS']) ) {
header("HTTP/1.1 500 Internal server error (database info incomplete)");
echo "Database info incomplete - Please set it manually";
exit;
}
 
// Connect to the database
mysqli_report(MYSQLI_REPORT_OFF);
if( !$dbHandle = @mysqli_connect($db['ZM_DB_HOST'], $db['ZM_DB_USER'], $db['ZM_DB_PASS'], $db['ZM_DB_NAME']) ) {
header("HTTP/1.1 500 Internal server error (can't connect to database)");
echo "Can't connect to database: ".mysqli_error($dbhandle);
exit;
}
 
// Query it for the file path
$query = "SELECT CONCAT(s.`path`, '/', e.`MonitorId`, '/', LEFT(e.`StartDateTime`, 10), '/', e.`Id`, '/', e.`defaultVideo`)
FROM `Events` e
INNER JOIN `Storage` s
ON s.`Id` = e.`StorageID`
WHERE e.`id` = '".mysqli_real_escape_string($dbHandle, $eventID)."'";
 
if( !$result = @mysqli_query($dbHandle, $query) ) {
header("HTTP/1.1 500 Internal server error (database query failed)");
echo "Database query failed: ".mysqli_error($dbHandle);
exit;
}
 
// Got a result?
if( (!$fullpath = @mysqli_fetch_row($result)) || empty($fullpath[0]) ) {
header("HTTP/1.1 500 Internal server error (empty result set or path)");
echo "Mysql returned empty result or path (bad event ID?)";
exit;
}
 
mysqli_close($dbHandle);
 
return $fullpath[0];
}
 
?></pre>
</div>
 
=== Testing and Usage ===
 
You can test the stream by opening it in a browser or a media player such as MPC-HC or VLC.
 
If you entered a filename or an event ID in the code then you can simply visit <kbd><nowiki>http://yourserver/replay-test.php</nowiki></kbd>
 
If you didn't enter a filename or event ID in the code then you'll need to add one to the url.
 
If you're using an event ID then the URL format is <kbd><nowiki>http://yourserver/replay-test.php?e=eventID</nowiki></kbd> (substituting the actual event ID for 'eventID')
 
If you want to use a file then the format is <kbd><nowiki>http://yourserver/replay-test.php?f=/full/path/to/file.mp4</nowiki></kbd>
 
If the video doesn't play and you're using a media player then try a browser - there'll probably be something useful in the output.
 
If you're using a browser and it just shows a black screen then the video might be H.265, which most of them don't support.


=== Setting up the monitor in ZM ===
=== Setting up the monitor in ZM ===


Set up a new monitor in ZM with a Source Type of FFMpeg, a Source Path of <kbd><nowiki>http://localhost/replay-test.php</nowiki></kbd>, and TCP as the Method.
To use this in ZM, set up a new monitor with a Source Type of FFMpeg, a Source Path URL as described in the Testing and Usage section above, and TCP as the Method.
 


That's it - You should be good to go and ZM should start capturing :)
That's it - You should be good to go and ZM should start capturing :)


=== Notes ===  
=== Notes ===  
Line 65: Line 209:
The best results will probably be obtained with videos that used Camera Passthrough as the writer because then ZM will see the same thing as it did the first time - other methods will be lossy and although the differences may be imperceptible to us, machines will spot them. If you must use an encoder then try to use a low CRF, such as 19 or even 17 to avoid too much degradation.
The best results will probably be obtained with videos that used Camera Passthrough as the writer because then ZM will see the same thing as it did the first time - other methods will be lossy and although the differences may be imperceptible to us, machines will spot them. If you must use an encoder then try to use a low CRF, such as 19 or even 17 to avoid too much degradation.


You can change the file to be replayed without editing the PHP by adding a <kbd>?f=/path/to/file.mp4</kbd> query string to the Source Path in the Monitor configuration (e.g. <kbd><nowiki>http://localhost/replay-test.php?f=/home/zoneminder/events/1/2021-11-30/173016/173016-video.mp4</nowiki></kbd>).
If you change the source video path or event ID in the PHP or if you overwrite the video file then you'll have stop and restart the monitor in ZM.
 
If you change the source video path in the PHP or if you overwrite the video file then you'll have stop and restart the monitor in ZM.


I find that the loop tends to stick at the end before restarting, but that could very well be the way my video is encoded and it may not happen to you.
I find that the loop tends to stick at the end before restarting, but that could very well be the way my video is encoded and it may not happen to you.


I suggest using sources with fairly long (a few frames or so) intros and outros without much happening in them to avoid spurious detection at the wraparound where the scene might change significantly (in ZM terms).
I suggest using sources with a few fairly static frames at their start end end to avoid spurious detection at the wraparound where the scene might change significantly (in ZM terms).


Comments, criticisms, and questions are welcome in [https://forums.zoneminder.com/viewtopic.php?f=11&t=31355 the forum thread]
Comments, questions, and criticisms are welcome in [https://forums.zoneminder.com/viewtopic.php?f=11&t=31355 the forum thread]


I think that's all for now - Good luck!
I think that's all for now - Good luck!

Latest revision as of 11:05, 3 December 2021

There was a question on the forums a while ago asking whether it was possible to test zones against stored events, which interested me, and I've cobbled together a method of doing it with PHP and FFMpeg. You can use it to stream a ZM event or a video file.

How it works

FFmpeg does all the real work, reading a video file at real-time speed and outputting it to the client. The rest of the code is a wrapper around the FFmpeg command, validating parameters and locating the file to be streamed.

Code

Create a file named replay-test.php in your server's web root (not the ZM root) and paste this into it. You shouldn't have to change anything, just copy/paste/save.

<?PHP

// Usage / ZM Source syntax
// To stream a ZM event: http://server/replay-test.php?e=eventID
// To stream a video file: http://server/replay-test.php?f=/full/path/to/file.mp4

// Source file
// Used only if you don't set an eventID below and don't
// pass a filename or event ID in the URL query string.
// Must be the /full/path/to/file.mp4
$infile = "";

// Event ID
// Used only if you don't pass an eventID or filename in the URL query string.
// Use 0 for none
$eventID = 0;

// Database info
// We need this when using event IDs.
// We'll try to get to get it from the
// ZM conf files so you'll only need to
// set it if that fails and you get an error.
// If you set one item then you must set all.
$db = array(    "ZM_DB_HOST" => "",
		"ZM_DB_NAME" => "",
		"ZM_DB_USER" => "",
		"ZM_DB_PASS" => "" );

// Whether to loop the video
// Use -1 for infinite,
// 0 for single-shot,
// 1 for 1 loop (2 plays), etc.
// Anything other than -1 will throw warnings in the ZM
// log when the stream ends and the zmc_ process restarts.
$loopcount = -1;

// Path to ffpmeg - try 'which ffmpeg' at the CLI to find yours
$ffmpeg = "/usr/bin/ffmpeg";

// Path to grep
$grep = "/bin/grep";

// No configuration variables below here

// URL parameters
if( !empty($_GET['f']) ) {
	$infile = $_GET['f'];
	$eventID = "";
}

if( !empty($_GET['e']) ) {
	$eventID = $_GET['e'];
}

// Got anything to do?
if( empty($infile) && empty($eventID) ) {
	header("HTTP/1.1 400 Bad Request (no filename or event ID)");
	echo "No filename or event ID";
	exit;
}

// Get a filepath if we've got an eventID
if( !empty($eventID) ) {
	$infile = getFilenameByEventID($eventID);
}

// Can we read the file?
if( empty($infile) || !is_readable($infile) ) {
	header("HTTP/1.1 500 Internal Server Error (can't read file)");
	echo "Can't read file '".$infile."')";
	exit;
}

// ffmpeg good to go?
if( !is_executable($ffmpeg) ) {
	header("HTTP/1.1 500 Internal server error (can't execute ffmpeg)");
	echo "Can't execute ffmpeg - please set the path";
	exit;
}

// Don't let max_execution_time kill us
set_time_limit(0);

// No output buffering or compression
// Try to cover all the bases...
@apache_setenv('no-gzip', 1);
@ini_set('output_buffering', 'Off');
@ini_set('output_handler', '');
@ini_set('zlib.output_compression', 'Off');
@ob_end_flush();
@flush;

// Tell the client what we're sending
header( "Content-Type: video/mp4" );

// And invoke FFmpeg to send it...
passthru( $ffmpeg." -stream_loop ".$loopcount." -re -fflags +genpts -i '".$infile."' -vcodec copy -an -sn -vsync 0 -map_metadata -1 -f mp4 -movflags frag_keyframe+empty_moov - 2>/dev/null");

// All done

exit;

function getFilenameByEventID( $eventID ) {
	// We'll need these
	// Don't like globals but don't want to pass anything other than eventID
	$db = $GLOBALS['db'];
	$grep = $GLOBALS['grep'];

	// Do we need to get database details from the .conf?
	if( empty($db['ZM_DB_HOST']) ) {
		if( !is_executable($grep) ) {
			header("HTTP/1.1 500 Internal server error (can't execute grep)");
			echo "Can't execute grep - please set the path";
			exit;
		}
		exec("$grep -hE \"^\s*ZM_DB_\" /etc/zm/zm.conf /etc/zm/conf.d/*.conf", $db);
		foreach($db as $key => $value) {
			unset($db[$key]);
			$newkey = trim(preg_replace("/^\s*(ZM_DB_[^=]*)=\s*(.*)\s*$/", "$1", $value ));
			$newvalue = trim(preg_replace("/^\s*(ZM_DB_[^=]*)=\s*(.*)\s*$/", "$2", $value ));
			if($newkey) {
				$db[$newkey] = $newvalue;
			}
		}
		unset($value);
	}

	// Got 'em all?
	if( empty($db['ZM_DB_HOST']) || empty($db['ZM_DB_NAME']) || empty($db['ZM_DB_USER']) || empty($db['ZM_DB_PASS']) ) {
		header("HTTP/1.1 500 Internal server error (database info incomplete)");
		echo "Database info incomplete - Please set it manually";
		exit;
	}

	// Connect to the database
	mysqli_report(MYSQLI_REPORT_OFF);
	if( !$dbHandle = @mysqli_connect($db['ZM_DB_HOST'], $db['ZM_DB_USER'], $db['ZM_DB_PASS'], $db['ZM_DB_NAME']) ) {
		header("HTTP/1.1 500 Internal server error (can't connect to database)");
		echo "Can't connect to database: ".mysqli_error($dbhandle);
		exit;
	}

	// Query it for the file path
	$query = "SELECT CONCAT(s.`path`, '/', e.`MonitorId`, '/', LEFT(e.`StartDateTime`, 10), '/', e.`Id`, '/', e.`defaultVideo`)
		FROM `Events` e
		INNER JOIN `Storage` s
		ON s.`Id` = e.`StorageID`
		WHERE e.`id` = '".mysqli_real_escape_string($dbHandle, $eventID)."'";

	if( !$result = @mysqli_query($dbHandle, $query) ) {
		header("HTTP/1.1 500 Internal server error (database query failed)");
		echo "Database query failed: ".mysqli_error($dbHandle);
		exit;
	}

	// Got a result?
	if( (!$fullpath = @mysqli_fetch_row($result)) || empty($fullpath[0]) ) {
		header("HTTP/1.1 500 Internal server error (empty result set or path)");
		echo "Mysql returned empty result or path (bad event ID?)";
		exit;
	}

	mysqli_close($dbHandle);

	return $fullpath[0];
}

?>

Testing and Usage

You can test the stream by opening it in a browser or a media player such as MPC-HC or VLC.

If you entered a filename or an event ID in the code then you can simply visit http://yourserver/replay-test.php

If you didn't enter a filename or event ID in the code then you'll need to add one to the url.

If you're using an event ID then the URL format is http://yourserver/replay-test.php?e=eventID (substituting the actual event ID for 'eventID')

If you want to use a file then the format is http://yourserver/replay-test.php?f=/full/path/to/file.mp4

If the video doesn't play and you're using a media player then try a browser - there'll probably be something useful in the output.

If you're using a browser and it just shows a black screen then the video might be H.265, which most of them don't support.

Setting up the monitor in ZM

To use this in ZM, set up a new monitor with a Source Type of FFMpeg, a Source Path URL as described in the Testing and Usage section above, and TCP as the Method.

That's it - You should be good to go and ZM should start capturing :)

Notes

The video is passed straight through rather than being transcoded so playback quality will be exactly as it was recorded in the file.

The best results will probably be obtained with videos that used Camera Passthrough as the writer because then ZM will see the same thing as it did the first time - other methods will be lossy and although the differences may be imperceptible to us, machines will spot them. If you must use an encoder then try to use a low CRF, such as 19 or even 17 to avoid too much degradation.

If you change the source video path or event ID in the PHP or if you overwrite the video file then you'll have stop and restart the monitor in ZM.

I find that the loop tends to stick at the end before restarting, but that could very well be the way my video is encoded and it may not happen to you.

I suggest using sources with a few fairly static frames at their start end end to avoid spurious detection at the wraparound where the scene might change significantly (in ZM terms).

Comments, questions, and criticisms are welcome in the forum thread

I think that's all for now - Good luck!