file name control

Hey, I’m super new to Wowza and the whole video streaming scene…

My situation:

  • Linux box with wowza installed and working perfectly

  • 2 Axis IP cameras

  • RTSP Stream being captured by wowza (rtp-record)

  • Stream auto started via startupstreams xml

My need:

  • To record multiple cameras for an unlimited duration, with the files being split into manageable (for searching purposes) chunks.

Im not sure how to accomplish this… Id like to have it store in a format like:

Camera1-YYY-MM-DD-HH.flv and automatically start a new file for each hour

I know VERY LITTLE JAVA so I get super lost looking at these code examples… I know my way around PHP/MySQL/Compiling stuff on linux etc… I can probably get eclipse to fire up and compile code if need be…

Is this possible? Can the recording module be setup to do this, can an event be triggered to stop/start the stream with a new stream name (reading the correct url from a file) every hour (crontab + shell script?)?

You can do it but you are going to have to take the plunge and compile a module.

https://www.wowza.com/downloads/WowzaIDE_beta_2/WowzaIDE_UsersGuide.pdf

Here a module I use for this purpose. It uses the name of the *.stream file as the recorded stream name.

It also requires the LiveStreamRecord addon package.

https://www.wowza.com/docs/how-to-record-live-streams-httplivestreamrecord

There was also an issue with Wowza trying to call the record command twice so you need to make sure that you are using the latest patch version of wowza.

https://www.wowza.com/downloads/WowzaMediaServer-2-1-2/WowzaMediaServer2.1.2-patch5.zip

Carefully ready the IMPORTANT!!! notice in the README.txt file of the patch. Several core components have been upgraded:


IMPORTANT!!!

Several core components have been updraded to newer versions. Before

applying this patch, delete the following files from your Wowza Media Server

installation:

[install-dir]/lib/bcprov-ext-jdk15-143.jar

[install-dir]/lib/commons-lang-2.4.jar

[install-dir]/lib/log4j-1.2.15.jar

You can set a startHour, endHour and timezone as properties in your Application.xml. Add them to the last properties section in the file.

It will record the files in a separate folder each day 00 - 31 in 1 hour long files. At the end of the month it will start overwriting the old files.

package streamrecord.hourly.monthlyrollover;
import java.io.File;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import com.wowza.wms.application.*;
import com.wowza.wms.module.*;
import com.wowza.wms.plugin.integration.liverecord.*;
import com.wowza.wms.stream.IMediaStream;
import com.wowza.wms.stream.IMediaStreamActionNotify;
import com.wowza.wms.stream.IMediaStreamNotify;
public class ModuleStreamRecord extends ModuleBase implements IMediaStreamNotify {
	private IApplicationInstance appInstance;
	private String timezone;
	private StreamTimer streamTimer;
	private int date;
	private int startHour = 0;
	private int endHour = 23;
	private int hourOfDay = -1;
	private boolean record = false;
	private PublishNotifier publishNotifier;
	private List<String> streams;
	
	public static final int FORMAT_UNKNOWN = 0;
	public static final int FORMAT_FLV = 1;
	public static final int FORMAT_MP4 = 2;
	
	private Map<String, ILiveStreamRecord> recorders = null;
	private class StreamTimer extends Thread {
		private boolean doQuit = false;
		public synchronized void quit() {
			doQuit = true;
		}
		public void run() {
			while (true) {
				try {
					TimeZone tz = TimeZone.getTimeZone(timezone);
					Calendar cal = Calendar.getInstance(tz);
					date = cal.get(Calendar.DATE);
					int prevHour = hourOfDay;
					hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
					int start = startHour;
					int end = endHour;
					if (start > end && hourOfDay > start) {
						end += 24;
					}
					if (end < start && hourOfDay < end) {
						start -=24;
					}
					if (hourOfDay >= start && hourOfDay < end) {
						record = true;
					} else {
						record = false;
					}
					streams = appInstance.getMediaCasterStreams().getMediaCasterNames();
					if (prevHour != hourOfDay && record) {
						for (String streamName : streams) {
							IMediaStream stream = appInstance.getStreams().getStream(streamName);
							appInstance.getVHost().getHandlerThreadPool().execute(new DoStartRecording(stream, streamName));
						}
					} else if(!record) {
						for (String streamName : streams) {
							ILiveStreamRecord recorder = recorders.remove(streamName);
							if(recorder != null)
								recorder.stopRecording();
						}
					}
					
					Thread.currentThread();
					Thread.sleep(60000);
					synchronized (this) {
						if (doQuit) {
							break;
						}
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	public void onAppStart(IApplicationInstance appInstance) {
		this.appInstance = appInstance;
		WMSProperties props = appInstance.getProperties();
		startHour = props.getPropertyInt("startHour", 0);
		endHour = props.getPropertyInt("endHour", 24);
		timezone = props.getPropertyStr("timezone", "America/Chicago");
		appInstance.addMediaStreamListener(this);
		recorders = Collections.synchronizedMap(new HashMap<String, ILiveStreamRecord>());
		publishNotifier = new PublishNotifier();
		
		streamTimer = new StreamTimer();
		streamTimer.setName("RecordController-" + appInstance.getApplication().getName());
		streamTimer.setDaemon(true);
		streamTimer.start();
	}
	public void onAppStop(IApplicationInstance appInstance) {
		streamTimer.quit();
		// cleanup any recorders that are still running
		synchronized (recorders) {
			Iterator<String> iter = recorders.keySet().iterator();
			while(iter.hasNext())
			{
				String streamName = iter.next();
				ILiveStreamRecord recorder = recorders.get(streamName);
				recorder.stopRecording();
				getLogger().info("  stopRecording: "+streamName);
			}
			recorders.clear();
		}
		
		recorders = null;
		appInstance.removeMediaStreamListener(this);
		publishNotifier = null;
	}
	private synchronized void startRecording(IMediaStream stream) {
		String streamAlias = stream.getName().substring(0, stream.getName().indexOf(".stream"));
		
		if (!streamAlias.isEmpty()) {
			String outputPath = appInstance.getStreamStorageDir()+"/"+String.format("%02d", date); //day;
			boolean append = false;
			File path = new File(outputPath);
			if (path.exists()) {
				File outputFile = new File(path.getPath()+File.separator+streamAlias+"_"+String.format("%02d", hourOfDay)+".mp4");
				Date today = new Date();
				if (!(outputFile.lastModified() < today.getTime() - 86400000)) {
					append = true;
				}
				
			} else {
				path.mkdirs();
			}
			
			recordStream(stream, FORMAT_MP4, append, outputPath+File.separator+streamAlias+"_"+String.format("%02d", hourOfDay)+".mp4", false, true, true);
		}
	}
	
	private void recordStream(IMediaStream stream, int format, boolean append, String outputPath, boolean versionFile, boolean startOnKeyFrame, boolean recordData)
	{
		String streamName = stream.getName();
		
		// if a format was not specified then check the stream prefix and choose accordingly
		if (format == FORMAT_UNKNOWN)
		{
			format = FORMAT_FLV;
			String extStr = stream.getExt();
			if (extStr.equals("mp4"))
				format = FORMAT_MP4;
		}
		
		String params = "stream:"+streamName;
		params += " format:"+(format==FORMAT_MP4?"mp4":"flv");
		params += " append:"+append;
		if (outputPath != null)
			params += " outputPath:"+outputPath;
		else
		{
			File writeFile = stream.getStreamFileForWrite();
			params += " outputPath:"+writeFile.getAbsolutePath();
		}
		params += " versionFile:"+versionFile;
		params += " startOnKeyFrame:"+startOnKeyFrame;
		params += " recordData:"+recordData;
		
		getLogger().info("ModuleStreamRecord.startRecording: "+params);
		
		// create a stream recorder and save it in a map of recorders
		ILiveStreamRecord recorder = null;
		
		// create the correct recorder based on format
		if (format == FORMAT_MP4)
			recorder = new LiveStreamRecorderMP4();
		else
			recorder = new LiveStreamRecorderFLV();
		
		// add it to the recorders list
		ILiveStreamRecord prevRecorder = recorders.get(streamName);
		if (prevRecorder != null)
			prevRecorder.stopRecording();
		recorders.put(streamName, recorder);
		
		// if you want to record data packets as well as video/audio
		recorder.setRecordData(recordData);
		
		// Set to true if you want to version the previous file rather than overwrite it
		recorder.setVersionFile(versionFile);
		
		// If recording only audio set this to false so the recording starts immediately
		recorder.setStartOnKeyFrame(startOnKeyFrame);
		
		// start recording
		recorder.startRecording(stream, outputPath, append);
	}
		@Override
		public void onMediaStreamCreate(IMediaStream stream) {
			stream.addClientListener(publishNotifier);
		}
		@Override
		public void onMediaStreamDestroy(IMediaStream stream) {
			stream.removeClientListener(publishNotifier);
			if(!stream.isPlay()) {
				ILiveStreamRecord recorder = recorders.remove(stream.getName());
				if (recorder != null)
					recorder.stopRecording();
			}
		}
		
	private class PublishNotifier implements IMediaStreamActionNotify {
		@Override
		public void onPause(IMediaStream stream, boolean isPause,
				double location) {
			// TODO Auto-generated method stub
			
		}
		@Override
		public void onPlay(IMediaStream stream, String streamName,
				double playStart, double playLen, int playReset) {
			// TODO Auto-generated method stub
			
		}
		@Override
		public void onPublish(IMediaStream stream, String streamName,
				boolean isRecord, boolean isAppend) {
			if(record)
				appInstance.getVHost().getHandlerThreadPool().execute(new DoStartRecording(stream, streamName));
		}
		@Override
		public void onSeek(IMediaStream stream, double location) {
			// TODO Auto-generated method stub
			
		}
		@Override
		public void onStop(IMediaStream stream) {
			// TODO Auto-generated method stub
			
		}
		@Override
		public void onUnPublish(IMediaStream stream, String streamName,
				boolean isRecord, boolean isAppend) {
			ILiveStreamRecord recorder = recorders.remove(streamName);
			if (recorder != null)
			{
				// stop recording
				recorder.stopRecording();
			}
		}
	}
	
	private class DoStartRecording implements Runnable {
		private IMediaStream stream;
		private String streamName;
		private DoStartRecording(IMediaStream stream, String streamName) {
			this.stream = stream;
			this.streamName = streamName;
		}
		public void run() {
			List<String> mediaCasters = appInstance.getMediaCasterStreams().getMediaCasterNames();
			if (mediaCasters.contains(streamName)) {
				int lockCount = appInstance.getMediaCasterStreams().getMediaCaster(streamName).getLockCount();
				if (lockCount > 0)
					startRecording(stream);
			}
		}
	}
}

Look at the tutorials for info on how to compile and deploy a module.

I guess the two things that are incorrect are:

Box “mdhd” has 2 extra bytes

Duration 22 Days, 03:14:34.381

The rest looks OK. The corrupt files may happen if you pull the stream from Wowza before the moov atom is written. The file is only closed after publishing stops.

Charlie

This patch fixes the warnings thrown by MP4Box:

WowzaMediaServer2.2.3-patch6.zip

Charlie

ModuleStreamRecord This needs to be the full class name ie. streamrecord.hourly.monthlyrollover.ModuleStreamRecord

The missing first hour was a bug that I never really got around to fixing. As it was running 24/7 it wasn’t too much of a problem.

To stop the original stream from recording, set the stream type to one of the non record types in the Application.xml.

Also, you do not need the following in your module.

ModuleLiveStreamRecord

ModuleLiveStreamRecord

com.wowza.wms.plugin.livestreamrecord.ModuleLiveStreamRecord

Your new module replaces this.

ERROR server comment - invoke(onAppStart): java.lang.NullPointerException: java.util.TimeZone.parseCustomTimeZone(TimeZone.java:712)

This is my fault :frowning: I had added some code to onAppStart to try and overcome the missing first hour. but put it in the wrong place.

	public void onAppStart(IApplicationInstance appInstance) {
		TimeZone tz = TimeZone.getTimeZone(timezone);
		Calendar cal = Calendar.getInstance(tz);
		hourOfDay = cal.get(Calendar.HOUR_OF_DAY);

		this.appInstance = appInstance;
		WMSProperties props = appInstance.getProperties();
		startHour = props.getPropertyInt("startHour", 0);
		endHour = props.getPropertyInt("endHour", 24);
		timezone = props.getPropertyStr("timezone", "America/Chicago");

The first 3 lines need to be moved to below the

timezone = props.getPropertyStr(“timezone”, “America/Chicago”);

like this

	public void onAppStart(IApplicationInstance appInstance) {
		this.appInstance = appInstance;
		WMSProperties props = appInstance.getProperties();
		startHour = props.getPropertyInt("startHour", 0);
		endHour = props.getPropertyInt("endHour", 24);
		timezone = props.getPropertyStr("timezone", "America/Chicago");

		TimeZone tz = TimeZone.getTimeZone(timezone);
		Calendar cal = Calendar.getInstance(tz);
		hourOfDay = cal.get(Calendar.HOUR_OF_DAY);

Where it was, timezone was null which is what the error was.

The recorder should be appending the stream each time the input from the camera stops and re-starts. If you have a look at your log files, do you see a lot of publish, unpublish events for the stream that corresponds to the small file? This could indicate a network problem to that camera.

When you view the stream live, do you get all the audio or is it cutting out there as well.

You can try forcing the cameras to send via tcp instead of udp. Use the ForceInterleaved setting here. https://www.wowza.com/docs/how-to-re-stream-video-from-an-ip-camera-rtsp-rtp-re-streaming.

			<Module> 
  				<Name>ModuleStreamRecord</Name> 
  				<Description>File Management</Description> 
  				<Class>streamrecord.hourly.monthlyrollover.ModuleStreamRecord</Class> 
 			</Module>
			<Module>
				<Name>ModuleLiveStreamRecord</Name>
				<Description>ModuleLiveStreamRecord</Description>
				<Class>com.wowza.wms.plugin.livestreamrecord.ModuleLiveStreamRecord</Class>
			</Module>

streamrecord.hourly.monthlyrollover.ModuleStreamRecord is an extension of com.wowza.wms.plugin.livestreamrecord.ModuleLiveStreamRecord so you do not need both. Remove com.wowza.wms.plugin.livestreamrecord.ModuleLiveStreamRecord and see what happens.

It is currently set to only work with mediacaster streams that are started using *.stream files to resolve the name for recording.

Also, as Richard mentions, when you are doing manual recording as in this case, you should set the stream type to live and not live-record. live-record will automatically record the stream but won’t roll over.

This probably requires StreamType “live”. You have “live-record”, which records every stream published to that app from start to finish.

Change StreamType to “live”

Do you need all those modules? Take out any that you are not using. Could be some conflicts.

Richard

According to these settings, they should be in /mnt/s3:

<Property>
<Name>fileMoverDestinationPath</Name>
<Value>/mnt/s3</Value>
</Property>
<Property>
<Name>fileMoverDeleteOriginal</Name>
<Value>true</Value>
<Type>Boolean</Type>

Change the 2nd Property above to false to keep original in /content folder

Richard

You’re welcome! I’m glad (assuming) it’s working.

Richard

Is this hourly rollover really the factor? Test by using StreamType “live-record” to record from that encoder. Do you get a good recording in that case?

Richard

Are there any ERROR or WARN lines in the error log on the hour, or after the app-start event?

Richard

Since you have this loaded up in the IDE, I suggest use the debugger, add a breakpoint and step through the code. And/or sprinkle in a bunch of getLogger statements to record what is happening on the hour.

Richard

Mamoor,

You can create a .stream file and making the contents the stream name you want to work with. These .stream files are commonly used to alias rtsp or upd URLs that are MediaCaster streams, but you can use it for RTMP stream in the same way: Make the contents of the .stream file the actual stream.

Richard

Do you try using a .stream file as I suggested?

/content/myStream.stream

Make the contents “myStream”, or whatever is actual stream name.

Richard

In my suggestion, the contents of test.stream would just be “test.sdp”. You are not re-streaming RTMP (like Liverepeater system), you are just aliasing the stream name.

Richard

Mamoor,

Sorry, one correction: .stream files are not right thing to use in this case. Instead use .play files. same concept but the file extension is .play. For example a file named myStream.play with contents “myStream”. If you are publishing a rtmp stream named myStream, you can then play the alias myStream.play

In the server side code you are using there is a least one line where “.stream” should be changed to “.play”, then recompiled. Look for other occurrences that need to be changed.

Richard

StreamManager is not used in this, and won’t work, it’s breaking it probably. Restart Wowza to get a clean start. After the encoder publishes myStream to the application, you can test the .play file in LiveVideoStreaming player (no stream manager):

Server: rtmp://[wowza-address]:1935/live

Stream: myStream.play

I tested with rtmp encoder using same names and Wowza player.

Richard

Thanks for the update. Glad it’s working. Sorry about not responding yesterday, an oversight.

Richard