Hi,
I am using XProtect Essential+ 2018 R2 with Milestone Mobile Server plugin installed. I have created a simple web application using the MIP Mobile SDK 2018 R2 JavaScript Library. My web application is accessed via Chrome 71.
In my web application, I have 4 camera feeds streaming from different cameras. Every few seconds, the camera feeds will change their video streams to stream from different cameras. After leaving the web application running for 1 day, I noticed the memory footprint and JavaScript memory increased to over 5 times the initial state.
The memory footprint and JavaScript memory did not get released after I close the camera feeds. Below is my HTML code of the camera feeds DOM elements.
HTML
<div id='video1'>
<canvas width='280' height='210'>
</div>
<div id='video2'>
<canvas width='280' height='210'>
</div>
<div id='video3'>
<canvas width='280' height='210'>
</div>
<div id='video4'>
<canvas width='280' height='210'>
</div>
Note: As I noticed from the sample provided by the mobile server plugin, the received frame is drawn on the canvas, hence my camera feeds only contains the canvas.
Below is my JavaScript code for loading the streams and closing the video streams.
JS
const cameraFeeds = {
cam1: {
cameraId: '',
imageURL: '',
streamRequest: null,
videoController: null,
videoConnectionObserver: null
},
cam2: {
cameraId: '',
imageURL: '',
streamRequest: null,
videoController: null,
videoConnectionObserver: null
},
cam3: {
cameraId: '',
imageURL: '',
streamRequest: null,
videoController: null,
videoConnectionObserver: null
},
cam4: {
cameraId: '',
imageURL: '',
streamRequest: null,
videoController: null,
videoConnectionObserver: null
}
}
// load video streams
function loadVideo( target, cameraId ) {
let retries = 5;
const canvas = document.querySelector( `#video${ target } canvas` );
const canvasContext = canvas.getContext( '2d' );
let drawing = false;
cameraFeeds[ `cam${ target }` ].videoConnectionObserver = {
videoConnectionReceivedFrame: videoConnectionReceivedFrame
};
cameraFeeds[ `cam${ target }` ].streamRequest = XPMobileSDK.requestStream( cameraId, { width: 280, height: 210 }, undefined, requestStreamCallback, requestStreamErrorCallback );
function requestStreamCallback( videoConnection ) {
if ( videoConnection !== null ) {
videoConnection.addObserver( cameraFeeds[ `cam${ target }` ].videoConnectionObserver );
videoConnection.open();
cameraFeeds[ `cam${ target }` ].videoController = videoConnection;
} else {
cameraFeeds[ `cam${ target }` ].streamRequest = XPMobileSDK.requestStream( cameraId, { width: 280, height: 210 }, undefined, requestStreamCallback, requestStreamErrorCallback );
}
}
function requestStreamErrorCallback( error ) {
if ( retries > 0 && cameraFeeds[ `cam${ target }` ].cameraId === cameraId ) {
retries--;
// timeout to avoid flooding the server
setTimeout( () => {
cameraFeeds[ `cam${ target }` ].streamRequest = XPMobileSDK.requestStream( cameraId, { width: 280, height: 210 }, undefined, requestStreamCallback, requestStreamErrorCallback );
}, 250);
}
}
function videoConnectionReceivedFrame( frame ) {
let image = document.createElement( 'img' );
image.addEventListener( 'load', onImageLoad );
image.addEventListener( 'error', onImageError );
drawing = true;
if ( frame.hasSizeInformation ) {
let multiplier = frame.sizeInfo.destinationSize.resampling * XPMobileSDK.getResamplingFactor();
image.width = multiplier * frame.sizeInfo.destinationSize.width;
image.height = multiplier * frame.sizeInfo.destinationSize.height;
}
if ( cameraFeeds[ `cam${ target }` ].imageUrl !== '' ) {
window.URL.revokeObjectURL( cameraFeeds[ `cam${ target }` ].imageUrl );
}
cameraFeeds[ `cam${ target }` ].imageUrl = window.URL.createObjectURL( frame.blob );
image.src = cameraFeeds[ `cam${ target }` ].imageUrl;
function onImageLoad( event ) {
canvas.width = image.width;
canvas.height = image.height;
canvasContext.drawImage( image, 0, 0, canvas.width, canvas.height );
drawing = false;
}
function onImageError( event ) {
drawing = false;
image = null;
}
}
}
// close video streams
function closeStream( target ) {
return new Promise( ( resolve, reject ) => {
const canvas = document.querySelector( `#video${ target } canvas` );
const canvasContext = canvas.getContext( '2d' );
if ( cameraFeeds[ `cam${ target }` ].videoController !== null && cameraFeeds[ `cam${ target }` ].videoController !== undefined ) {
cameraFeeds[ `cam${ target }` ].videoController.removeObserver( cameraFeeds[ `cam${ target }` ].videoConnectionObserver );
XPMobileSDK.closeStream( cameraFeeds[ `cam${ target }` ].videoController.videoId );
cameraFeeds[ `cam${ target }` ].videoController.destroy();
cameraFeeds[ `cam${ target }` ].videoController = null;
}
if ( cameraFeeds[ `cam${ target }` ].streamRequest !== null && cameraFeeds[ `cam${ target }` ].streamRequest !== undefined ) {
XPMobileSDK.cancelRequest( cameraFeeds[ `cam${ target }` ].streamRequest );
cameraFeeds[ `cam${ target }` ].streamRequest = null;
}
window.URL.revokeObjectURL( cameraFeeds[ `cam${ target }` ].imageURL );
cameraFeeds[ `cam${ target }` ].imageURL = '';
cameraFeeds[ `cam${ target }` ].cameraId = '';
cameraFeeds[ `cam${ target }` ].videoConnectionObserver = null;
canvasContext.clearRect( 0, 0, canvas.width, canvas.height );
resolve();
} );
}
Whenever the camera feeds changes the video stream, it will close the existing stream by calling closeStream. After closeStream function has completed, loadVideo will be called.
I am not sure what might cause the memory leak and why the memory is not being released after stopping and closing all the camera feeds.
Is this the correct way of closing the camera streams and changing the source?