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.
Code: Select all
<?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 = 0;
}
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];
}
?>
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 yourserver/replay-test.php?e=eventID (substituting the actual event ID for 'eventID')
If you want to use a file then the format is 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.
Once you've verified that it's working you can set up a monitor in ZM with a Source Type of FFMpeg, a Source Path as described above, and TCP as the Method.
That's it - You should be good to go and ZM should start capturing
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).
I think that's all - Good luck!