Handling 24 video streams simultaneously from the Milestone Mobile Server in a single HTML (Angular) page

We have an issue when trying to show 24 live/playback streams at the same time on a single HTML page. Every camera image (canvas) that shows frame images is scaled so 24 streams can fit into a single HTML page. The customer has requested that all streams are shown in full HD so we can provide them with digital zoom functionality on every camera that is shown on the screen. The main problem, in our opinion, can be that we receive full HD images and redraw those images on significantly smaller canvas sizes. That can be task with a high CPU requirements and hard to execute on the web browser. To be able to fulfill our customer requirements we have the following questions.

1. When the HTML page size is changed, that involves a change of the canvas that shows the frames. How we can in live or playback, while receiving frames from the stream, change the resolution of the frame that we need?

2. How to handle digital zoom functionality over the mobile server without requesting full HD images for every camera? How to request part of the full HD images with top/left offsets and defined with and height, while we are already connected to the stream?

3. While receiving frames from the mobile server, how we can receive information is camera connected/disconnected/have any connectivity issue? The main problem is that when the camera is disconnected we still receive the same frame from the mobile server, without any other information that the camera has a connectivity issue. Is it possible to receive that information while we are connected to the stream over the mobile server?

Below is a short part of the code that we use for implementing the solution. This pattern is taken from one of the Milestone examples that came with milestone installation. We would appreciate it if you can provide us with instructions on how to accomplish those requirements. If you have any advice on how we should connect and handle streams, we are keen to follow your advice and modify implementation so we can provide the customer with the requested functionality.

The code sample is below:

---------------------

var cameraId = id; // This is parameter that we receive from the Milestone
var signalType = 'Live';  // Or 'Playback'
var inWidth = this.width; // Width of canvas dom element
var inHeight = this.height; // Height of canvas dom element
 
var streamRequestObj = <any>{
	CameraId: cameraId,
	DestWidth: inWidth,
	DestHeight: inHeight,
	SignalType: signalType /*'Live' or 'Playback'*/,
	MethodType: 'Push' /*'Pull'*/,
	Fps: 25,
	ComprLevel: 71,
	KeyFramesOnly: 'No' /*'Yes'*/,
	RequestSize: 'Yes',
	StreamType: 'Transcoded', //'FragmentedMP4', // 'Transcoded'
	// SrcWidth: inWidth,
	// SrcHeight: inHeight,
};
if (this.isPlaybackMode) {
	toRet.PlaybackControllerId = this.playbackController.id;
}
 
var videoConnectionReceivedFrame = (frame) => {
	if (frame.dataSize > 0) {
		if (frame.hasSizeInformation) {
			var multiplier = (frame.sizeInfo.destinationSize.resampling * XPMobileSDK.getResamplingFactor()) || 1;
			this.image.width = multiplier * frame.sizeInfo.destinationSize.width;
			this.image.height = multiplier * frame.sizeInfo.destinationSize.height;
		}
		if (this.imageURL) {
			window.URL.revokeObjectURL(this.imageURL);
		}
		this.imageURL = window.URL.createObjectURL(frame.blob);
		this.image.src = this.imageURL;
	} else {
		//empty frame
	}
}
 
this.videoConnectionObserver = {
	videoConnectionReceivedFrame: videoConnectionReceivedFrame
}
 
var requestStreamCallback = (videoConnection) => {
	this.videoController = videoConnection;
	videoConnection.addObserver(this.videoConnectionObserver);
	videoConnection.open();
 
	this.camera_relatedAudioGuid = this.camera_relatedAudioGuid,
	this.videoConnection = videoConnection;
}
 
this.streamRequest = XPMobileSDK.RequestStream(streamRequestObj, requestStreamCallback, function (error) {
	console.warn('Error on request live stream.');
});
 
// Option for playback
// this.streamRequest = XPMobileSDK.RequestStream(streamRequestObj, requestStreamCallback, function (error) {
// 	console.warn('Error on request playback stream.');
// });
 
 
this.image = document.createElement('img');
this.image.addEventListener('load', onImageLoad);
this.image.addEventListener('error', onImageError);
 
var onImageLoad = (event) => {
	var zoomedImage = scaleIt(this.image, this.canvasDomElementContext.canvas);
	this.canvasDomElementContext.drawImage(zoomedImage, 0, 0, this.canvasDomElementContext.canvas.width, this.canvasDomElementContext.canvas.height);
}
 
var onImageError = (event) => {
	console.warn('Error on image loading.');
}

Hi @Zeljko Tepic

Let me try to answer you question one by one.

So about the first one - how to resize the stream on-the-fly?

There is a dedicated command for resizing named changeStream.

In order to understand how to use it, please add following snippet to your code:

		var counter = 0;
 
		setInterval(() => {
			counter++;
			var videoConnectionSize = {};
			videoConnectionSize.width = 800;
			videoConnectionSize.height = 600;
			if (counter % 2 == 0) {
				videoConnectionSize.width = 80;
				videoConnectionSize.height = 60;
			}
			var videoConnectionCropping = {};
			
			// videoController
			// VideoConnectionCropping, left","top","right","bottom
			// VideoConnectionSize,  "width", "height"
			XPMobileSDK.changeStream(videoController, videoConnectionCropping, videoConnectionSize);
		},
		10000);

As you can imagine, it alters the steam resolution from 800 x 600 to 80 x 60 (an vice versa) on every 10 seconds.

I’m sure you can calculate your needed resolution (subscribing to window resize events, etc…). After that just need to call the change stream command with correct parameters.

About the second one - how to implement digital zoom.

You can crop (part of) the original image with the same command “changeStream” using the other parameter - videoConnectionCropping.

I’ve tried to make a sample in order (you) to understand how to use it:

            var srcWidth = 1920;
	    var srcHeight = 1080;
	    function videoConnectionReceivedFrame(frame) {
...
	            if (frame.hasSizeInformation) {
	                var multiplier = (frame.sizeInfo.destinationSize.resampling * XPMobileSDK.getResamplingFactor()) || 1;
	                image.width = multiplier * frame.sizeInfo.destinationSize.width;
	                image.height = multiplier * frame.sizeInfo.destinationSize.height;
 
					srcWidth = frame.sizeInfo.sourceSize.width;
					srcHeight = frame.sizeInfo.sourceSize.height;
	            }
...
	        }
	    }
 
		var counter = 0;
 
		setInterval(() => {
			counter++;
			var videoConnectionSize = {};
			videoConnectionSize.width = 800;
			videoConnectionSize.height = 600;
 
			var videoConnectionCropping = {};
			videoConnectionCropping.left = 1;
			videoConnectionCropping.top = 1;
			videoConnectionCropping.right = srcWidth;
			videoConnectionCropping.bottom = srcHeight;
 
			if (counter % 2 == 0) {
				videoConnectionCropping.right /= 2;
				videoConnectionCropping.bottom /= 2;
			}
			
			// videoController
			// VideoConnectionCropping, left","top","right","bottom
			// VideoConnectionSize,  "width", "height"
			XPMobileSDK.changeStream(videoController, videoConnectionCropping, videoConnectionSize);
		},
		10000);

The sample above alters the cropping from “no cropping” to left top quarter of the screen on every 10 seconds.

Having knowledge on where you want to zoom, you could calculate what part of the original picture you want to crop in order to achieve similar to zoom functionality. It is a matter of correct calculations.

Something that is not exactly intuitive is the cropping rectangle and how to convert from relative cropping like (0, 0, 0.5, 0.5) to absolute cropping - the one that is received by the API.

For this to happen, you need to preserve the original source width and height, returned by the server. As seen in the sample, they are received in the video frame receive callback. Namely in frame.sizeInfo.sourceSize object. Please bear in mind, what is received in that object could vary from the original camera resolution, but exactly this has to be used in the cropping info sent to the server.

In that particular example relative cropping (0, 0, 0.5, 0.5) is to be converted to absolute one like (0, 0, srcWidth/2, srcHeight/2).

btw. we call that mode server-side zoom.

Third part - receiving information for camera connectivity.

First of all I really doubt that you receive more actual video frames, once camera is disconnected. I do believe there should be some glitch in the code and one and the same old image is displayed on the screen.

About the live status events - they are send on every change in the stream. Please look at the sample bellow:

function videoConnectionReceivedFrame(frame) {
...
			if (frame.hasLiveInformation) {
				console.log("Current Live Events: " + frame.currentLiveEvents);
			}
...
}

The “currentLiveEvents” filed is a bitfield mask and contains information about the camera live status.

Possible values are described in the documentation. I’ll cite them here for completeness:

• LiveFeed = 1,

• Motion = 2,

• Recording = 4,

• Notification = 8,

• CameraConnectionLost = 16,

• DatabaseFail = 32,

• DiskFull = 64,

• ClientLiveStopped = 128,

What I’ve received on my side testing it:

// Start streaming

Current Live Events: 1 - LiveFeed

// Disable camera in MC

Current Live Events: 0 - Nothing

Current Live Events: 144 - CameraConnectionLost | ClientLiveStopped

Current Live Events: 16 - CameraConnectionLost

Current Live Events: 144 - CameraConnectionLost | ClientLiveStopped

Current Live Events: 16 - CameraConnectionLost

// Enable camera again in MC

Current Live Events: 129 - LiveFeed | ClientLiveStopped

So, basically you have to look for 1 and 16. They are the most reliable ones.