How to use a video file as a Source

From ZoneMinder Wiki
Jump to navigationJump to search

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!