const fs = require('fs');
const URL = require('url');
const events = require('events');
const Mp4Frag = require('mp4frag');
const treekill = require('tree-kill');
const exec = require('child_process').exec;
const spawn = require('child_process').spawn;
const connectionTester = require('connection-tester')
const SoundDetection = require('shinobi-sound-detection')
const streamViewerCountTimeouts = {}
const { createQueueAwaited } = require('../common.js')
module.exports = (s,config,lang) => {
    const {
        applyPartialToConfiguration,
        getWarningChangesForMonitor,
        createPipeArray,
        splitForFFMPEG,
        sanitizedFfmpegCommand,
    } = require('../ffmpeg/utils.js')(s,config,lang)
    const {
        buildSubstreamString,
        getDefaultSubstreamFields,
    } = require('../ffmpeg/builders.js')(s,config,lang)
    const {
        addEventDetailsToString,
        closeEventBasedRecording,
        convertRegionPointsToNewDimensions,
        triggerEvent,
    } = require('../events/utils.js')(s,config,lang)
    const {
        setHomePositionPreset,
    } = require('../control/ptz.js')(s,config,lang)
    const {
        scanForOrphanedVideos,
        reEncodeVideoAndBinOriginalAddToQueue,
    } = require('../video/utils.js')(s,config,lang)
    const {
        selectNodeForOperation,
        bindMonitorToChildNode
    } = require('../childNode/utils.js')(s,config,lang)
    const isMasterNode = (
        (
            config.childNodes.enabled === true &&
            config.childNodes.mode === 'master'
        ) ||
        config.childNodes.enabled === false
    );
    const getUpdateableFields = require('./updatedFields.js')
    const processKill = (proc) => {
        const response = {ok: true}
        const processPID = proc && proc.pid ? parseInt(`${proc.pid}`) : null
        return new Promise((resolve,reject) => {
            let alreadyResolved = false
            function doResolve(response){
                if(alreadyResolved)return;
                alreadyResolved = true;
                resolve(response)
            }
            if(!proc || !processPID){
                response.msg = 'No Process to Kill'
                doResolve(response)
                return
            }
            function sendError(err){
                response.ok = false
                response.err = err
                doResolve(response)
            }
            function lastResort(){
                treekill(processPID)
                response.msg = 'treekill'
                doResolve(response)
            }
            try{
                proc.removeAllListeners()
                proc.on('exit',() => {
                    response.msg = 'proc.on.exit'
                    clearTimeout(killTimer)
                    doResolve(response)
                    treekill(processPID)
                });
                if(proc && proc.stdin) {
                    proc.stdin.write("q\r\n");
                }
                let killTimer = setTimeout(() => {
                    if(proc && proc.kill){
                        if(s.isWin){
                            response.msg = 'taskkill'
                            spawn("taskkill", ["/pid", processPID, '/t'])
                        }else{
                            response.msg = 'SIGTERM'
                            proc.kill('SIGTERM')
                        }
                        killTimer = setTimeout(lastResort,3000)
                    }
                },1000)
            }catch(err){
                s.debugLog(err)
                sendError(err)
            }
        })
    }
    const cameraDestroy = async function(e,p){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const theGroup = s.group[groupKey]
        if(
            theGroup &&
            theGroup.activeMonitors[e.id] &&
            theGroup.activeMonitors[e.id].spawn !== undefined
        ){
            const activeMonitor = s.group[groupKey].activeMonitors[e.id];
            const proc = s.group[groupKey].activeMonitors[e.id].spawn;
            if(proc){
                activeMonitor.allowStdinWrite = false
                s.txToDashcamUsers({
                    f : 'disable_stream',
                    ke : groupKey,
                    mid : e.id
                },groupKey)
    //            if(activeMonitor.p2pStream){activeMonitor.p2pStream.unpipe();}
                try{
                    proc.removeListener('end',activeMonitor.spawn_exit);
                    proc.removeListener('exit',activeMonitor.spawn_exit);
                    delete(activeMonitor.spawn_exit);
                }catch(er){

                }
            }
            if(activeMonitor.audioDetector){
              activeMonitor.audioDetector.stop()
              delete(activeMonitor.audioDetector)
            }
            activeMonitor.firstStreamChunk = {}
            clearTimeout(activeMonitor.recordingChecker);
            delete(activeMonitor.recordingChecker);
            clearTimeout(activeMonitor.streamChecker);
            delete(activeMonitor.streamChecker);
            clearTimeout(activeMonitor.timelapseFramesChecker);
            delete(activeMonitor.timelapseFramesChecker);
            clearTimeout(activeMonitor.checkSnap);
            delete(activeMonitor.checkSnap);
            clearTimeout(activeMonitor.watchdog_stop);
            delete(activeMonitor.watchdog_stop);
            // delete(activeMonitor.secondaryDetectorOutput);
            delete(activeMonitor.detectorFrameSaveBuffer);
            clearTimeout(activeMonitor.recordingSnapper);
            clearInterval(activeMonitor.getMonitorCpuUsage);
            clearInterval(activeMonitor.objectCountIntervals);
            clearTimeout(activeMonitor.timeoutToRestart)
            clearTimeout(activeMonitor.fatalErrorTimeout);
            delete(activeMonitor.onvifConnection)
            // if(activeMonitor.onChildNodeExit){
            //     activeMonitor.onChildNodeExit()
            // }
            try{
                activeMonitor.spawn.stdio.forEach(function(stdio){
                  try{
                    stdio.unpipe()
                  }catch(err){
                    console.log(err)
                  }
                })
            }catch(err){
                // s.debugLog(err)
            }
            if(activeMonitor.mp4frag){
                var mp4FragChannels = Object.keys(activeMonitor.mp4frag)
                mp4FragChannels.forEach(function(channel){
                    activeMonitor.mp4frag[channel].removeAllListeners()
                    delete(activeMonitor.mp4frag[channel])
                })
            }
            if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
                s.cx({f:'clearCameraFromActiveList',ke:groupKey,id:e.id})
            }
            if(activeMonitor.childNode){
                s.cx({f:'kill',d:s.cleanMonitorObject(e)},activeMonitor.childNodeId)
            }else{
                const killResponse = await processKill(proc);
                s.debugLog(`cameraDestroy`,killResponse)
                activeMonitor.allowDestroySubstream = true
                const killSubResponse = await destroySubstreamProcess(activeMonitor)
                if(killSubResponse.hadSubStream)s.debugLog(`cameraDestroy`,killSubResponse.closeResponse)
            }
        }
    }
    function copyMonitorConfiguration(groupKey,monitorId){
        return Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId])
    }
    function getMonitorConfiguration(groupKey,monitorId){
        return s.group[groupKey].rawMonitorConfigurations[monitorId]
    }
    function getActiveMonitor(groupKey,monitorId){
        return s.group[groupKey].activeMonitors[monitorId]
    }
    const createSnapshot = (options) => {
        const url = options.url
        const streamDir = options.streamDir || s.dir.streams
        const inputOptions = options.input || []
        const outputOptions = options.output || []
        return new Promise((resolve,reject) => {
            if(!url){
                resolve(null);
                return
            }
            const completeRequest = () => {
                fs.readFile(temporaryImageFile,(err,imageBuffer) => {
                    fs.rm(temporaryImageFile,(err) => {
                        if(err){
                            s.debugLog(err)
                        }
                    })
                    if(err){
                        s.debugLog(err)
                    }
                    resolve(imageBuffer)
                })
            }
            const temporaryImageFile = streamDir + s.gid(5) + '.jpg'
            const ffmpegCmd = splitForFFMPEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
            const snapProcess = spawn('ffmpeg',ffmpegCmd,{detached: true})
            snapProcess.stderr.on('data',function(data){
                // s.debugLog(data.toString())
            })
            snapProcess.on('close',async function(data){
                clearTimeout(snapProcessTimeout)
                completeRequest()
            })
            var snapProcessTimeout = setTimeout(function(){
                processKill(snapProcess).then((response) => {
                    s.debugLog(`createSnapshot-snapProcessTimeout`,response)
                    completeRequest()
                })
            },5000)
        })
    }
    const addCredentialsToStreamLink = (options) => {
        const streamUrl = options.url
        const username = options.username
        const password = options.password
        const urlParts = streamUrl.split('://')
        urlParts[0] = 'http'
        return ['rtsp','://',`${username}:${password}@`,urlParts[1]].join('')
    }
    const monitorConfigurationMigrator = (monitor) => {
        // converts the old style to the new style.
        const updatedFields = getUpdateableFields()
        const fieldKeys = Object.keys(updatedFields)
        fieldKeys.forEach((oldKey) => {
            if(oldKey === 'details'){
                const detailKeys = Object.keys(updatedFields.details)
                detailKeys.forEach((oldKey) => {
                    if(oldKey === 'stream_channels'){
                        if(monitor.details.stream_channels){
                            const channelUpdates = updatedFields.details.stream_channels
                            const channelKeys = Object.keys(channelUpdates)
                            const streamChannels = s.parseJSON(monitor.details.stream_channels) || []
                            streamChannels.forEach(function(channel,number){
                                channelKeys.forEach((oldKey) => {
                                    const newKey = channelUpdates[oldKey]
                                    monitor.details.stream_channels[number][newKey] = streamChannels[number][oldKey] ? streamChannels[number][oldKey] : monitor.details.stream_channels[number][newKey]
                                    // delete(e.details.stream_channels[number][oldKey])
                                })
                            })
                        }
                    }else{
                        const newKey = updatedFields.details[oldKey]
                        monitor.details[newKey] = monitor.details[oldKey] ? monitor.details[oldKey] : monitor.details[newKey]
                        // delete(monitor.details[oldKey])
                    }
                })
            }else{
                const newKey = updatedFields[oldKey]
                monitor[newKey] = monitor[oldKey] ? monitor[oldKey] : monitor[newKey]
                // delete(monitor[oldKey])
            }
        })
    }
    const spawnSubstreamProcess = function(e){
        // e = monitorConfig
        try{
            const groupKey = e.ke
            const monitorId = e.mid || e.id
            const monitorConfig = copyMonitorConfiguration(groupKey,monitorId)
            const monitorDetails = monitorConfig.details
            const activeMonitor = getActiveMonitor(groupKey,monitorId)
            const channelNumber = 1 + (monitorDetails.stream_channels || []).length
            const ffmpegCommand = [`-progress pipe:5`];
            const logLevel = monitorDetails.loglevel ? e.details.loglevel : 'warning'
            const stdioPipes = createPipeArray({}, 2)
            const substreamConfig = monitorConfig.details.substream
            substreamConfig.input.type = !substreamConfig.input.fulladdress ? monitorConfig.type : substreamConfig.input.type || monitorConfig.details.rtsp_transport
            substreamConfig.input.fulladdress = substreamConfig.input.fulladdress || s.buildMonitorUrl(monitorConfig)
            substreamConfig.input.rtsp_transport = substreamConfig.input.rtsp_transport || monitorConfig.details.rtsp_transport
            const {
                inputAndConnectionFields,
                outputFields,
            } = getDefaultSubstreamFields(monitorConfig);
            ([
                buildSubstreamString(channelNumber + config.pipeAddition,e),
            ]).forEach(function(commandStringPart){
                ffmpegCommand.push(commandStringPart)
            });
            const ffmpegCommandString = ffmpegCommand.join(' ')
            activeMonitor.ffmpegSubstream = sanitizedFfmpegCommand(e,ffmpegCommandString)
            const ffmpegCommandParsed = splitForFFMPEG(ffmpegCommandString)
            activeMonitor.subStreamChannel = channelNumber;
            s.userLog({
                ke: groupKey,
                mid: monitorId,
            },
            {
                type: lang["Substream Process"],
                msg: {
                    msg: lang["Process Started"],
                    cmd: ffmpegCommandString,
                },
            });
            const subStreamProcess = spawn(config.ffmpegDir,ffmpegCommandParsed,{detached: true,stdio: stdioPipes})
            attachStreamChannelHandlers({
                ke: groupKey,
                mid: e.mid,
                fields: Object.assign({},inputAndConnectionFields,outputFields),
                number: activeMonitor.subStreamChannel,
                ffmpegProcess: subStreamProcess,
            })
            if(config.debugLog === true){
                subStreamProcess.stderr.on('data',(data) => {
                    console.log(`${groupKey} ${monitorId}`)
                    console.log(data.toString())
                })
            }

            subStreamProcess.stderr.on('data',(data) => {
                const string = data.toString();
                if(string.includes('No such')){
                    processKill(subStreamProcess);
                    return;
                }
                if(logLevel !== 'quiet'){
                    s.userLog({
                        ke: groupKey,
                        mid: monitorId,
                    },{
                        type: lang["Substream Process"],
                        msg: string
                    })
                }
            });

            subStreamProcess.stdio[5].on('data',(data) => {
                resetStreamCheck({
                    ke: groupKey,
                    mid: monitorId,
                })
            });

            subStreamProcess.on('close',(data) => {
                if(!activeMonitor.allowDestroySubstream){
                    subStreamProcess.stderr.on('data',(data) => {
                        s.userLog({
                            ke: groupKey,
                            mid: monitorId,
                        },
                        {
                            type: lang["Substream Process"],
                            msg: lang["Process Crashed for Monitor"],
                        })
                    })
                    setTimeout(() => {
                        spawnSubstreamProcess(e)
                    },2000)
                }
            })
            activeMonitor.subStreamProcess = subStreamProcess
            s.tx({
                f: 'substream_start',
                mid: monitorId,
                ke: groupKey,
                channel: activeMonitor.subStreamChannel
            },'GRP_'+groupKey);
            return subStreamProcess
        }catch(err){
            s.systemLog(err)
            return null
        }
    }
    const destroySubstreamProcess = async function(activeMonitor){
        // e = monitorConfig.details.substream
        const response = {
            hadSubStream: false,
            alreadyClosing: false
        }
        try{
            if(activeMonitor.subStreamProcessClosing){
                response.alreadyClosing = true
            }else if(activeMonitor.subStreamProcess){
                activeMonitor.subStreamProcessClosing = true
                activeMonitor.subStreamChannel = null;
                const closeResponse = await processKill(activeMonitor.subStreamProcess)
                response.hadSubStream = true
                response.closeResponse = closeResponse
                delete(activeMonitor.subStreamProcess)
                s.tx({
                    f: 'substream_end',
                    mid: activeMonitor.mid,
                    ke: activeMonitor.ke
                },'GRP_'+activeMonitor.ke);
                activeMonitor.subStreamProcessClosing = false
            }
        }catch(err){
            s.debugLog('destroySubstreamProcess',err)
        }
        return response
    }
    function attachStreamChannelHandlers(options){
        const fields = options.fields
        const number = options.number
        const ffmpegProcess = options.ffmpegProcess
        const activeMonitor = s.group[options.ke].activeMonitors[options.mid]
        const pipeNumber = number + config.pipeAddition;
        if(!activeMonitor.emitterChannel[pipeNumber]){
            activeMonitor.emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0);
        }
       let frameToStreamAdded
       switch(fields.stream_type){
           case'mp4':
               delete(activeMonitor.mp4frag[pipeNumber])
               if(!activeMonitor.mp4frag[pipeNumber])activeMonitor.mp4frag[pipeNumber] = new Mp4Frag();
               ffmpegProcess.stdio[pipeNumber].pipe(activeMonitor.mp4frag[pipeNumber],{ end: false })
           break;
           case'mjpeg':
               frameToStreamAdded = function(d){
                   activeMonitor.emitterChannel[pipeNumber].emit('data',d)
               }
           break;
           case'flv':
               frameToStreamAdded = function(d){
                   if(!activeMonitor.firstStreamChunk[pipeNumber])activeMonitor.firstStreamChunk[pipeNumber] = d;
                   frameToStreamAdded = function(d){
                       activeMonitor.emitterChannel[pipeNumber].emit('data',d)
                   }
                   frameToStreamAdded(d)
               }
           break;
           case'h264':
               frameToStreamAdded = function(d){
                   activeMonitor.emitterChannel[pipeNumber].emit('data',d)
               }
           break;
        }
        if(frameToStreamAdded){
            ffmpegProcess.stdio[pipeNumber].on('data',frameToStreamAdded)
        }
    }
    function setActiveViewer(groupKey,monitorId,connectionId,isBeingAdded){
        const viewerList = s.group[groupKey].activeMonitors[monitorId].watch;
        if(isBeingAdded){
            if(viewerList.indexOf(connectionId) > -1)viewerList.push(connectionId);
        }else{
            viewerList.splice(viewerList.indexOf(connectionId), 1)
        }
        const numberOfViewers = viewerList.length
        s.tx({
            f: 'viewer_count',
            viewers: numberOfViewers,
            ke: groupKey,
            id: monitorId
        },'MON_' + groupKey + monitorId)
        return numberOfViewers;
    }
    function getActiveViewerCount(groupKey,monitorId){
        const viewerList = s.group[groupKey].activeMonitors[monitorId].watch;
        const numberOfViewers = viewerList.length
        return numberOfViewers;
    }
    function setTimedActiveViewerForHttp(req){
        const groupKey = req.params.ke
        const connectionId = req.params.auth
        const loggedInUser = s.group[groupKey].users[connectionId]
        if(!loggedInUser){
            const monitorId = req.params.id
            const viewerList = s.group[groupKey].activeMonitors[monitorId].watch
            const theViewer = viewerList[connectionId]
            if(!theViewer){
                setActiveViewer(groupKey,monitorId,connectionId,true)
            }
            clearTimeout(streamViewerCountTimeouts[req.originalUrl])
            streamViewerCountTimeouts[req.originalUrl] = setTimeout(() => {
                setActiveViewer(groupKey,monitorId,connectionId,false)
            },5000)
        }else{
            s.debugLog(`User is Logged in, Don't add to viewer count`);
        }
    }
    function attachMainProcessHandlers(e,fatalError){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId);
        activeMonitor.spawn_exit = async function(){
            if(activeMonitor.isStarted === true){
                if(e.details.loglevel !== 'quiet'){
                    s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:activeMonitor.ffmpeg}});
                }
                await fatalError(e,'Process Unexpected Exit');
                scanForOrphanedVideos(e,{
                    forceCheck: true,
                    checkMax: 2
                })
                const monitorConfig = copyMonitorConfiguration(groupKey,monitorId);
                s.onMonitorUnexpectedExitExtensions.forEach(function(extender){
                    extender(monitorConfig,e)
                })
            }
        }
        activeMonitor.spawn.on('end',activeMonitor.spawn_exit)
        activeMonitor.spawn.on('exit',activeMonitor.spawn_exit)
        activeMonitor.spawn.on('error',function(er){
            s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error')
        })
        s.userLog(e,{
            type: lang['Process Started'],
            msg: {
                cmd: activeMonitor.ffmpeg
            }
        })
    }
    async function deleteMonitorData(groupKey,monitorId){
        // deleteVideos
        // deleteFileBinFiles
        // deleteTimelapseFrames
        async function deletePath(thePath){
            try{
                await fs.promises.stat(thePath)
                await fs.promises.rm(thePath, {recursive: true})
            }catch(err){

            }
        }
        async function deleteFromTable(tableName){
            await s.knexQueryPromise({
                action: "delete",
                table: tableName,
                where: {
                    ke: groupKey,
                    mid: monitorId,
                }
            })
        }
        async function getSizeFromTable(tableName){
            const response = await s.knexQueryPromise({
                action: "select",
                columns: "size",
                table: tableName,
                where: {
                    ke: groupKey,
                    mid: monitorId,
                }
            })
            const rows = response.rows
            let size = 0
            for (let i = 0; i < rows.length; i++) {
                const row = rows[i]
                size += row.size
            }
            return size
        }
        async function adjustSpaceCounterForTableWithAddStorage(tableName,storageType){
            // does normal videos and addStorage
            const response = await s.knexQueryPromise({
                action: "select",
                columns: "ke,mid,details,size",
                table: tableName || 'Videos',
                where: {
                    ke: groupKey,
                    mid: monitorId,
                }
            })
            const rows = response.rows
            for (let i = 0; i < rows.length; i++) {
                const video = rows[i]
                const storageIndex = s.getVideoStorageIndex(video)
                if(storageIndex){
                    s.setDiskUsedForGroupAddStorage(video.ke,{
                        size: -(video.size / 1048576),
                        storageIndex: storageIndex
                    },storageType)
                }else{
                    s.setDiskUsedForGroup(video.ke,-(video.size / 1048576),storageType)
                }
            }
        }
        async function adjustSpaceCounter(tableName,storageType){
            const amount = await getSizeFromTable(tableName)
            s.setDiskUsedForGroup(groupKey,-amount,storageType)
        }
        const videosDir = s.dir.videos + `${groupKey}/${monitorId}`
        const binDir = s.dir.fileBin + `${groupKey}/${monitorId}`

        // videos and addStorage
        await adjustSpaceCounterForTableWithAddStorage('Timelapse Frames','timelapseFrames')
        await adjustSpaceCounterForTableWithAddStorage('Videos')
        await deleteFromTable('Videos')
        await deletePath(videosDir)
        for (let i = 0; i < s.dir.addStorage.length; i++) {
            const storage = s.dir.addStorage[i]
            const addStorageDir = storage.path + groupKey + '/' + monitorId
            await deletePath(addStorageDir)
            await deletePath(addStorageDir + '_timelapse')
        }

        // timelapse frames
        await adjustSpaceCounter('Timelapse Frames','timelapseFrames')
        await deleteFromTable('Timelapse Frames')
        await deletePath(videosDir + '_timelapse')

        // fileBin
        await adjustSpaceCounter('Files','fileBin')
        await deleteFromTable('Files')
        await deletePath(binDir)
    }
    async function deleteMonitor(options){
        const response = { ok: true }
        try{
            const user = options.user
            const userId = user.uid
            const groupKey = options.ke
            const monitorId = options.id || options.mid
            const deleteFiles = options.deleteFiles === undefined ? true : options.deleteFiles
            s.userLog({
                ke: groupKey,
                mid: monitorId
            },{
                type: lang.monitorDeleted,
                msg: `${lang.byUser} : ${userId}`
            });
            s.camera('stop', {
                ke: groupKey,
                mid: monitorId,
                delete: 1,
            });
            s.tx({
                f: 'monitor_delete',
                uid: userId,
                mid: monitorId,
                ke: groupKey
            },`GRP_${groupKey}`);
            await s.knexQueryPromise({
                action: "delete",
                table: "Monitors",
                where: {
                    ke: groupKey,
                    mid: monitorId,
                }
            });
            if(deleteFiles){
                await deleteMonitorData(groupKey,monitorId)
                s.debugLog(`Deleted Monitor Data`,{
                    ke: groupKey,
                    mid: monitorId,
                });
            }
            response.msg = `${lang.monitorDeleted} ${lang.byUser} : ${userId}`
        }catch(err){
            response.ok = false
            response.err = err
            s.systemLog(err)
        }
        return response
    }
    function getUrlProtocol(urlString){
        let modifiedUrlString = `${urlString}`.split('://')
        const originalProtocol = `${modifiedUrlString[0]}`
        return originalProtocol
    }
    function modifyUrlProtocol(urlString,newProtocol){
        let modifiedUrlString = `${urlString}`.split('://')
        const originalProtocol = `${modifiedUrlString[0]}`
        modifiedUrlString[0] = newProtocol;
        modifiedUrlString = modifiedUrlString.join('://')
        return modifiedUrlString
    }
    function getUrlParts(urlString){
        const originalProtocol = getUrlProtocol(urlString)
        const modifiedUrlString = modifyUrlProtocol(urlString,'http')
        const url = URL.parse(modifiedUrlString)
        const data = {}
        Object.keys(url).forEach(function(key){
            const value = url[key];
            if(value && typeof value !== 'function')data[key] = url[key];
        });
        data.href = `${urlString}`
        data.origin = modifyUrlProtocol(data.origin,originalProtocol)
        data.protocol = `${originalProtocol}:`
        return data
    }
    async function monitorStop(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        if(!s.group[groupKey]||!s.group[groupKey].activeMonitors[monitorId]){return}
        const activeMonitor = getActiveMonitor(groupKey,monitorId);
        const monitorConfig = copyMonitorConfiguration(groupKey,monitorId);
        const monitorOnChildNode = config.childNodes.enabled === true && config.childNodes.mode === 'master' && activeMonitor.childNode && s.childNodes[activeMonitor.childNode].activeCameras[groupKey+monitorId];
        if(monitorOnChildNode){
            activeMonitor.isStarted = false
            s.cx({
                f:'sync',
                sync: monitorConfig,
                ke: groupKey,
                mid: monitorId
            },activeMonitor.childNodeId);
            s.cx({
                f: 'cameraStop',
                d: monitorConfig
            },activeMonitor.childNodeId);
        }else{
            closeEventBasedRecording(e)
            if(activeMonitor.fswatch){activeMonitor.fswatch.close();delete(activeMonitor.fswatch)}
            if(activeMonitor.fswatchStream){activeMonitor.fswatchStream.close();delete(activeMonitor.fswatchStream)}
            if(activeMonitor.last_frame){delete(activeMonitor.last_frame)}
            if(activeMonitor.isStarted !== true){return}
            await cameraDestroy(e)
            clearTimeout(activeMonitor.trigger_timer)
            delete(activeMonitor.trigger_timer)
            clearInterval(activeMonitor.detector_notrigger_timeout)
            clearTimeout(activeMonitor.fatalErrorTimeout);
            activeMonitor.isStarted = false
            activeMonitor.isRecording = false
            s.tx({f:'monitor_stopping',mid:monitorId,ke:groupKey,time:s.formattedTime()},'GRP_'+groupKey);
            s.cameraSendSnapshot({mid:monitorId,ke:groupKey,mon:e},{useIcon: true})
            s.userLog(e,{type:lang['Monitor Stopped'],msg:lang.MonitorStoppedText});
            clearTimeout(activeMonitor.delete)
            if(e.delete === 1){
                activeMonitor.delete = setTimeout(function(){
                    delete(s.group[groupKey].activeMonitors[monitorId]);
                    delete(s.group[groupKey].rawMonitorConfigurations[monitorId]);
                },1000 * 20);
            }
        }
        s.sendMonitorStatus({
            id: monitorId,
            ke: groupKey,
            status: lang.Stopped,
            code: 5,
        });
        if(isMasterNode){
            setTimeout(() => {
                scanForOrphanedVideos({
                    ke: groupKey,
                    mid: monitorId,
                },{
                    forceCheck: true,
                    checkMax: 2
                })
            },2000)
        }
        s.onMonitorStopExtensions.forEach(function(extender){
            extender(monitorConfig,e)
        })
    }
    function monitorIdle(e){
        const monitorId = e.mid || e.id
        const groupKey = e.ke
        s.tx({f:'monitor_idle',mid:monitorId,ke:groupKey,time:s.formattedTime()},'GRP_'+groupKey);
        s.userLog(e,{type:lang['Monitor Idling'],msg:lang.MonitorIdlingText});
        s.sendMonitorStatus({
            id: monitorId,
            ke: groupKey,
            status: lang.Idle,
            code: 6,
        })
    }
    async function monitorRestart(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        s.sendMonitorStatus({
            id: monitorId,
            ke: groupKey,
            status: lang.Restarting,
            code: 4,
        });
        await s.camera('stop',e)
        if(e.mode !== 'restart')await s.camera(`${e.mode}`,e);
    }
    function monitorAddViewer(e,cn){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId);
        if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}}
        if(!cn.monitorsCurrentlyWatching[monitorId]){cn.monitorsCurrentlyWatching[monitorId] = { ke: groupKey }}
        setActiveViewer(groupKey,monitorId,cn.id,true)
        activeMonitor.allowDestroySubstream = false
        clearTimeout(activeMonitor.noViewerCountDisableSubstream)
    }
    function monitorRemoveViewer(e,cn){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId);
        if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[monitorId])}
        setActiveViewer(groupKey,monitorId,cn.id,false)
        clearTimeout(activeMonitor.noViewerCountDisableSubstream)
        activeMonitor.noViewerCountDisableSubstream = setTimeout(async () => {
            let currentCount = getActiveViewerCount(groupKey,monitorId)
            if(currentCount === 0 && activeMonitor.subStreamProcess){
                activeMonitor.allowDestroySubstream = true
                await destroySubstreamProcess(activeMonitor)
            }
        },10000)
    }
    function createRecordingDirectory(e,callback){
        var directory;
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){
            //addStorage choice
            directory = s.checkCorrectPathEnding(e.details.dir) + groupKey + '/'
            fs.mkdir(directory,function(err){
                s.handleFolderError(err)
                directory = directory + monitorId + '/'
                fs.mkdir(directory,function(err){
                    s.handleFolderError(err)
                    callback(err,directory)
                })
            })
        }else{
            //MAIN videos dir
            directory = s.dir.videos + groupKey + '/'
            fs.mkdir(directory,function(err){
                s.handleFolderError(err)
                directory = s.dir.videos + groupKey + '/' + monitorId + '/'
                fs.mkdir(directory,function(err){
                    s.handleFolderError(err)
                    callback(err,directory)
                })
            })
        }
    }
    function createTimelapseDirectory(e,callback){
        var directory = s.getTimelapseFrameDirectory(e)
        fs.mkdir(directory,{ recursive: true },function(err){
            s.handleFolderError(err)
            callback(err,directory)
        })
    }
    function createFileBinDirectory(e,callback){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        var directory = s.dir.fileBin + groupKey + '/'
        fs.mkdir(directory,function(err){
            s.handleFolderError(err)
            directory = s.dir.fileBin + groupKey + '/' + monitorId + '/'
            fs.mkdir(directory,function(err){
                s.handleFolderError(err)
                callback(err,directory)
            })
        })
    }
    function createStreamDirectory(e,callback){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        callback = callback || function(){}
        var directory = s.dir.streams + groupKey + '/'
        fs.mkdir(directory,function(err){
            directory = s.dir.streams + groupKey + '/' + monitorId + '/'
            s.handleFolderError(err)
            fs.mkdir(directory,function(err){
                if (err){
                    s.handleFolderError(err)
                    s.file('deleteFolder',directory + '*',function(err){
                        callback(err,directory)
                    })
                }else{
                    callback(err,directory)
                }
            })
        })
    }
    function createCameraFolders(e,callback){
        return new Promise((resolve) => {
            //set the recording directory
            const groupKey = e.ke
            const monitorId = e.mid || e.id
            var activeMonitor = s.group[groupKey].activeMonitors[monitorId]
            createStreamDirectory(e,function(err,directory){
                activeMonitor.sdir = directory
                e.sdir = directory
                createRecordingDirectory(e,function(err,directory){
                    activeMonitor.dir = directory
                    e.dir = directory
                    createTimelapseDirectory(e,function(err,directory){
                        activeMonitor.dirTimelapse = directory
                        e.dirTimelapse = directory
                        createFileBinDirectory(e,function(err){
                            if(callback)callback()
                            resolve()
                        })
                    })
                })
            })
        })
    }
    async function forceMonitorRestart(monitor,restartMessage){
        const groupKey = monitor.ke
        const monitorId = monitor.mid
        const monitorConfig = copyMonitorConfiguration(groupKey,monitorId)
        s.sendMonitorStatus({
            id: monitorId,
            ke: groupKey,
            status: lang.Restarting,
            code: 4,
        })
        await launchMonitorProcesses(monitorConfig)
        s.userLog({
            ke: groupKey,
            mid: monitorId,
        },restartMessage)
        scanForOrphanedVideos({
            ke: groupKey,
            mid: monitorId,
        },{
            forceCheck: true,
            checkMax: 2
        })
    }
    function stripAuthFromHost(e){
        var host = e.host.split('@');
        if(host[1]){
            //username and password found
            host = host[1]
        }else{
            //no username or password in `host` string
            host = host[0]
        }
        return host
    }
    function resetRecordingCheck(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        clearTimeout(activeMonitor.recordingChecker)
        const segmentLengthInMinutes = !isNaN(parseFloat(e.details.cutoff)) ? parseFloat(e.details.cutoff) : '15'
        const segmentLength = e.type === 'dashcam' ? segmentLengthInMinutes * 100 : segmentLengthInMinutes
        const monitorConfig = getMonitorConfiguration(groupKey,monitorId);
        activeMonitor.recordingChecker = setTimeout(function(){
            if(activeMonitor.isStarted === true && monitorConfig.mode === 'record'){
                forceMonitorRestart({
                    ke: groupKey,
                    mid: monitorId,
                },{
                    type: lang['Camera is not recording'],
                    msg: {
                        msg: lang['Restarting Process']
                    }
                })
            }
        },60000 * segmentLength * 1.3);
    }
    function resetStreamCheck(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        clearTimeout(activeMonitor.streamChecker)
        activeMonitor.streamChecker = setTimeout(function(){
            if(activeMonitor && activeMonitor.isStarted === true){
                forceMonitorRestart({
                    ke: groupKey,
                    mid: monitorId,
                },{
                    type: lang['Camera is not streaming'],
                    msg: {
                        msg: lang['Restarting Process']
                    }
                })
            }
        },60000*1);
    }
    function resetTimelapseFramesCheck(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
        const creationInterval = parseFloat(monitorConfig.details.record_timelapse_fps) || 900;
        clearTimeout(activeMonitor.timelapseFramesChecker)
        activeMonitor.timelapseFramesChecker = setTimeout(function(){
            if(activeMonitor && activeMonitor.isStarted === true){
                forceMonitorRestart({
                    ke: groupKey,
                    mid: monitorId,
                },{
                    type: lang['Camera is not recording'],
                    msg: {
                        msg: lang['Restarting Process']
                    }
                })
            }
        }, 1000 * creationInterval * 2);
    }
    function onDetectorJpegOutputAlone(e,d){
        if(s.isAtleatOneDetectorPluginConnected){
            const groupKey = e.ke
            const monitorId = e.mid || e.id
            const monitorConfig = getMonitorConfiguration(groupKey,monitorId);
            s.ocvTx({
                f: 'frame',
                mon: monitorConfig.details,
                ke: groupKey,
                id: monitorId,
                time: s.formattedTime(),
                frame: d
            })
        }
    }
    function onDetectorJpegOutputSecondary(e,buffer){
        if(s.isAtleatOneDetectorPluginConnected){
            const groupKey = e.ke
            const monitorId = e.mid || e.id
            const activeMonitor = getActiveMonitor(groupKey,monitorId)
            const theArray = activeMonitor.pipe4BufferPieces
            theArray.push(buffer)
            if(buffer[buffer.length-2] === 0xFF && buffer[buffer.length-1] === 0xD9){
                activeMonitor.secondaryDetectorOutput.emit('data',Buffer.concat(theArray))
                activeMonitor.pipe4BufferPieces = []
            }
        }
    }
    async function createCameraFfmpegProcess(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        var activeMonitor = getActiveMonitor(groupKey,monitorId)
        //launch ffmpeg (main)
        s.tx({
            f: 'monitor_starting',
            mode: e.functionMode,
            mid: monitorId,
            time: s.formattedTime()
        },'GRP_'+groupKey)
        activeMonitor.spawn = await s.ffmpeg(e)
        if(activeMonitor.spawn){
            attachMainProcessHandlers(e,fatalError)
        }else{
            s.debugLog(`Failed to Launch Monitor!`)
        }
        return activeMonitor.spawn
    }
    function createEventCounter(monitor){
        if(monitor.details.detector_obj_count === '1'){
            const activeMonitor = s.group[monitor.ke].activeMonitors[monitor.id]
            activeMonitor.eventsCountStartTime = new Date()
            clearInterval(activeMonitor.objectCountIntervals)
            activeMonitor.objectCountIntervals = setInterval(() => {
                const eventsCounted = activeMonitor.eventsCounted || {}
                const countsToSave = Object.assign(eventsCounted,{})
                activeMonitor.eventsCounted = {}
                const groupKey = monitor.ke
                const monitorId = monitor.id
                const startTime = new Date(activeMonitor.eventsCountStartTime + 0)
                const endTime = new Date()
                const countedKeys = Object.keys(countsToSave)
                activeMonitor.eventsCountStartTime = new Date()
                if(countedKeys.length > 0)countedKeys.forEach((tag) => {
                    const tagInfo = countsToSave[tag]
                    const count = Object.keys(tagInfo.count)
                    const times = tagInfo.times
                    const realTag = tagInfo.tag
                    s.knexQuery({
                        action: "insert",
                        table: "Events Counts",
                        insert: {
                            ke: groupKey,
                            mid: monitorId,
                            details: JSON.stringify({
                                times: times,
                                count: count,
                            }),
                            time: startTime,
                            end: endTime,
                            count: count.length,
                            tag: realTag
                        }
                    })
                })
            },60000) //every minute
        }
    }
    function createCameraStreamHandlers(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        const detectorEnabled = e.details.detector === '1'
        activeMonitor.spawn.stdio[5].on('data',function(data){
            resetStreamCheck(e)
        })
        //emitter for mjpeg
        if(!e.details.stream_mjpeg_clients||e.details.stream_mjpeg_clients===''||isNaN(e.details.stream_mjpeg_clients)===false){e.details.stream_mjpeg_clients=20;}else{e.details.stream_mjpeg_clients=parseInt(e.details.stream_mjpeg_clients)}
        activeMonitor.emitter = new events.EventEmitter().setMaxListeners(e.details.stream_mjpeg_clients);
        if(detectorEnabled && e.details.detector_audio === '1'){
            if(activeMonitor.audioDetector){
              activeMonitor.audioDetector.stop()
              delete(activeMonitor.audioDetector)
            }
            var triggerLevel
            var triggerLevelMax
            if(e.details.detector_audio_min_db && e.details.detector_audio_min_db !== ''){
                triggerLevel = parseInt(e.details.detector_audio_min_db)
            }else{
                triggerLevel = 5
            }
            if(e.details.detector_audio_max_db && e.details.detector_audio_max_db !== ''){
                triggerLevelMax = parseInt(e.details.detector_audio_max_db)
            }
            var audioDetector = new SoundDetection({
                format: {
                    bitDepth: 16,
                    numberOfChannels: 1,
                    signed: true
                },
                triggerLevel: triggerLevel,
                triggerLevelMax: triggerLevelMax
            },function(dB) {
                triggerEvent({
                    f: 'trigger',
                    id: monitorId,
                    ke: groupKey,
                    details: {
                        plug: 'audio',
                        name: 'db',
                        reason: 'soundChange',
                        confidence: dB
                    }
                })
            })
            activeMonitor.audioDetector = audioDetector
            audioDetector.start()
            activeMonitor.spawn.stdio[6].pipe(audioDetector.streamDecoder,{ end: false })
        }
        if(e.details.record_timelapse === '1'){
            var timelapseRecordingDirectory = s.getTimelapseFrameDirectory(e)
            activeMonitor.spawn.stdio[7].on('data', function(data){
                try{
                    var fileStream = activeMonitor.recordTimelapseWriter
                    if(!fileStream){
                        var currentDate = s.formattedTime(null,'YYYY-MM-DD')
                        var filename = s.formattedTime() + '.jpg'
                        var location = timelapseRecordingDirectory + currentDate + '/'
                        if(!fs.existsSync(location)){
                            fs.mkdirSync(location)
                        }
                        fileStream = fs.createWriteStream(location + filename)
                        fileStream.on('error', err => s.debugLog(err))
                        fileStream.on('close', function () {
                            activeMonitor.recordTimelapseWriter = null
                            s.createTimelapseFrameAndInsert(e,location,filename)
                            resetTimelapseFramesCheck(e)
                        })
                        activeMonitor.recordTimelapseWriter = fileStream
                    }
                    fileStream.write(data)
                    clearTimeout(activeMonitor.recordTimelapseWriterTimeout)
                    activeMonitor.recordTimelapseWriterTimeout = setTimeout(function(){
                        fileStream.end()
                    },900)
                }catch(err){
                    s.debugLog(err)
                }
            })
        }
        if(e.details.detector === '1'){
            s.ocvTx({f:'init_monitor',id:monitorId,ke:groupKey})
            //frames from motion detect
            if(e.details.detector_pam === '1'){
               // activeMonitor.spawn.stdio[3].pipe(activeMonitor.p2p).pipe(activeMonitor.pamDiff)
               // spawn.stdio[3] is deprecated and now motion events are handled by dataPort
                if(e.details.detector_use_detect_object === '1' && e.details.detector_use_motion === '1' ){
                    activeMonitor.spawn.stdio[4].on('data',function(data){
                        onDetectorJpegOutputSecondary(e,data)
                    })
                }else{
		            activeMonitor.spawn.stdio[4].on('data',function(data){
                        onDetectorJpegOutputAlone(e,data)
                    })
		        }
            }else if(e.details.detector_use_detect_object === '1' && e.details.detector_send_frames !== '1'){
                activeMonitor.spawn.stdio[4].on('data',function(data){
                    onDetectorJpegOutputSecondary(e,data)
                })
            }else{
                activeMonitor.spawn.stdio[4].on('data',function(data){
                    onDetectorJpegOutputAlone(e,data)
                })
            }
        }
        //frames to stream
       var frameToStreamPrimary;
       const streamType = e.details.stream_type;
       switch(streamType){
           case'mp4':
               delete(activeMonitor.mp4frag['MAIN'])
               if(!activeMonitor.mp4frag['MAIN'])activeMonitor.mp4frag['MAIN'] = new Mp4Frag()
               activeMonitor.mp4frag['MAIN'].on('error',function(error){
                   s.userLog(e,{type:lang['Mp4Frag'],msg:{error:error}})
               })
               activeMonitor.spawn.stdio[1].pipe(activeMonitor.mp4frag['MAIN'],{ end: false })
           break;
           case'flv':
               frameToStreamPrimary = function(d){
                   if(!activeMonitor.firstStreamChunk['MAIN'])activeMonitor.firstStreamChunk['MAIN'] = d;
                   frameToStreamPrimary = function(d){
                       resetStreamCheck(e)
                       activeMonitor.emitter.emit('data',d)
                   }
                   frameToStreamPrimary(d)
               }
           break;
           case'mjpeg':
               frameToStreamPrimary = function(d){
                   resetStreamCheck(e)
                   activeMonitor.emitter.emit('data',d)
               }
           break;
           case'b64':case undefined:case null:case'':
               var buffer
               frameToStreamPrimary = function(d){
                  resetStreamCheck(e)
                  if(!buffer){
                      buffer=[d]
                  }else{
                      buffer.push(d)
                  }
                  if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){
                      activeMonitor.emitter.emit('data',Buffer.concat(buffer))
                      buffer = null
                  }
               }
           break;
        }
        s.onMonitorCreateStreamPipeExtensions.forEach(function(extender){
            if(!frameToStreamPrimary)frameToStreamPrimary = extender(streamType,e,resetStreamCheck)
        });
        if(frameToStreamPrimary){
            activeMonitor.spawn.stdout.on('data',frameToStreamPrimary)
        }
        if(e.details.stream_channels && e.details.stream_channels !== ''){
            s.parseJSON(e.details.stream_channels,{}).forEach((fields,number) => {
                attachStreamChannelHandlers({
                    ke: groupKey,
                    mid: monitorId,
                    fields: fields,
                    number: number,
                    ffmpegProcess: activeMonitor.spawn,
                })
            })
        }
    }
    function catchNewSegmentNames(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        const monitorConfig = getMonitorConfiguration(groupKey,monitorId)
        const monitorDetails = monitorConfig.details
        const autoCompressionEnabled = monitorDetails.auto_compress_videos === '1'
        var checkLog = function(d,x){return d.indexOf(x)>-1}
        activeMonitor.spawn.stdio[8].on('data',function(d){
            d=d.toString();
            if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(d)){
                var filename = d.split('.')[0].split(' [')[0].trim()+'.'+e.ext
                s.insertCompletedVideo(e,{
                    file: filename,
                    events: activeMonitor.detector_motion_count
                },function(err,response){
                    s.userLog(e,{type:lang['Video Finished'],msg:{filename:d}})
                    if(
                        e.details.detector === '1' &&
                        activeMonitor.isStarted === true &&
                        e.details &&
                        e.details.detector_record_method === 'del'&&
                        e.details.detector_delete_motionless_videos === '1'&&
                        activeMonitor.detector_motion_count.length === 0
                    ){
                        if(e.details.loglevel !== 'quiet'){
                            s.userLog(e,{type:lang['Delete Motionless Video'],msg:filename})
                        }
                        s.deleteVideo({
                            filename : filename,
                            ke : groupKey,
                            id : monitorId
                        })
                    }else if(autoCompressionEnabled){
                        s.debugLog('Queue Automatic Compression',response.insertQuery)
                        reEncodeVideoAndBinOriginalAddToQueue({
                            video: response.insertQuery,
                            targetExtension: 'webm',
                            doSlowly: false,
                            automated: true,
                        }).then((encodeResponse) => {
                            s.debugLog('Complete Automatic Compression',encodeResponse)
                        }).catch((err) => {
                            console.log(err)
                        })
                    }
                    activeMonitor.detector_motion_count = []
                })
                resetRecordingCheck(e)
            }
        })
    }
    async function doFatalErrorCatch(e,d){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        if(activeMonitor.isStarted === true){
            const activeMonitor = getActiveMonitor(groupKey,monitorId)
            activeMonitor.isStarted = false
            await cameraDestroy(e)
            activeMonitor.isStarted = true
            fatalError(e,d)
        }else{
            await cameraDestroy(e)
        }
    }
    function cameraFilterFfmpegLog(e){
        var checkLog = function(d,x){return d.indexOf(x)>-1}
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        activeMonitor.spawn.stderr.on('data',async function(d){
            d=d.toString();
            switch(true){
                case checkLog(d,'Not Enough Bandwidth'):
                    activeMonitor.criticalErrors['453'] = true
                break;
                case checkLog(d,'No space left on device'):
                    s.checkUserPurgeLock(groupKey)
                    s.purgeDiskForGroup(groupKey)
                break;
                case checkLog(d,'error parsing AU headers'):
                    s.userLog(e,{type:lang['Error While Decoding'],msg:lang.ErrorWhileDecodingTextAudio});
                break;
                case checkLog(d,'error while decoding'):
                    s.userLog(e,{type:lang['Error While Decoding'],msg:lang.ErrorWhileDecodingText});
                break;
                case checkLog(d,'pkt->duration = 0'):
                case checkLog(d,'[hls @'):
                case checkLog(d,'Past duration'):
                case checkLog(d,'Last message repeated'):
                case checkLog(d,'Non-monotonous DTS'):
                case checkLog(d,'NULL @'):
                case checkLog(d,'RTP: missed'):
                case checkLog(d,'deprecated pixel format used'):
                    return
                break;
                case checkLog(d,'Could not find tag for vp8'):
                case checkLog(d,'Only VP8 or VP9 Video'):
                case checkLog(d,'Could not write header'):
                    return s.userLog(e,{type:lang['Incorrect Settings Chosen'],msg:{msg:d}})
                break;
                case checkLog(d,'Connection refused'):
                case checkLog(d,'Connection timed out'):
                case checkLog(d,'Immediate exit requested'):
                case checkLog(d,'mjpeg_decode_dc'):
                case checkLog(d,'bad vlc'):
                case checkLog(d,'does not contain an image sequence pattern or a pattern is invalid.'):
                case checkLog(d,'error dc'):
                    // activeMonitor.timeoutToRestart = setTimeout(() => {
                    //     doFatalErrorCatch(e,d)
                    // },15000)
                break;
                case checkLog(d,'Could not find codec parameters'):
                case checkLog(d,'No route to host'):
                    activeMonitor.timeoutToRestart = setTimeout(async () => {
                        doFatalErrorCatch(e,d)
                    },60000)
                break;
            }
            s.userLog(e,{type:"FFMPEG STDERR",msg:d})
        })
    }
    function setNoEventsDetector(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        var detector_notrigger_timeout = (parseFloat(e.details.detector_notrigger_timeout) || 10) * 1000 * 60
        var currentConfig = getMonitorConfiguration(groupKey,monitorId).details
        clearInterval(s.group[groupKey].activeMonitors[monitorId].detector_notrigger_timeout)
        s.group[groupKey].activeMonitors[monitorId].detector_notrigger_timeout = setInterval(function(){
            if(currentConfig.detector_notrigger_webhook === '1' && !s.group[groupKey].activeMonitors[monitorId].detector_notrigger_webhook){
                s.group[groupKey].activeMonitors[monitorId].detector_notrigger_webhook = s.createTimeout('detector_notrigger_webhook',s.group[groupKey].activeMonitors[monitorId],currentConfig.detector_notrigger_webhook_timeout,10)
                var detector_notrigger_webhook_url = addEventDetailsToString(e,currentConfig.detector_notrigger_webhook_url)
                var webhookMethod = currentConfig.detector_notrigger_webhook_method
                if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET';
                fetchTimeout(detector_notrigger_webhook_url,10000,{
                    method: webhookMethod
                }).catch((err) => {
                    s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}})
                })
            }
            if(currentConfig.detector_notrigger_command_enable === '1' && !s.group[groupKey].activeMonitors[monitorId].detector_notrigger_command){
                s.group[groupKey].activeMonitors[monitorId].detector_notrigger_command = s.createTimeout('detector_notrigger_command',s.group[groupKey].activeMonitors[monitorId],currentConfig.detector_notrigger_command_timeout,10)
                var detector_notrigger_command = addEventDetailsToString(e,currentConfig.detector_notrigger_command)
                if(detector_notrigger_command === '')return
                exec(detector_notrigger_command,{detached: true},function(err){
                    if(err)s.debugLog(err)
                })
            }
            s.onDetectorNoTriggerTimeoutExtensions.forEach(function(extender){
                extender(e)
            })
        },detector_notrigger_timeout)
    }
    function setMotionLock(e){
        const monitorId = e.mid || e.id
        const groupKey = e.ke
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        if(e.details.detector_trigger === '1'){
            clearTimeout(activeMonitor.motion_lock)
            activeMonitor.motion_lock = setTimeout(function(){
                clearTimeout(activeMonitor.motion_lock)
                delete(activeMonitor.motion_lock)
            },15000)
        }
    }
    function asyncConnectionTest(host,port,timeout){
        return new Promise((resolve) => {
            connectionTester.test(host,port,timeout,(err,response) => {
                resolve({
                    err,
                    response,
                })
            })
        })
    }
    async function launchMonitorProcesses(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const theGroup = s.group[groupKey]
        const activeMonitor = theGroup.activeMonitors[monitorId]
        const isMacOS = s.platform !== 'darwin';
        const isWatchOnly = e.functionMode === 'start'
        const isRecord = e.functionMode === 'record'
        const isWatchOnlyOrRecord = isWatchOnly || isRecord;
        const streamTypeIsJPEG = e.details.stream_type === 'jpeg'
        const streamTypeIsHLS = e.details.stream_type === 'hls'
        const jpegApiEnabled = e.details.snap === '1'
        const typeIsDashcam = e.type === 'dashcam' || e.type === 'socket'
        const typeIsMjpeg = e.type === 'mjpeg'
        const typeIsH264 = e.type === 'h264'
        const typeIsLocal = e.type === 'local'
        const monitorConfig = theGroup.rawMonitorConfigurations[monitorId]
        const doPingTest = e.type !== 'socket' && e.type !== 'dashcam' && e.protocol !== 'udp' && e.type !== 'local' && e.details.skip_ping !== '1';
        if(!theGroup.startMonitorInQueue){
            theGroup.startMonitorInQueue = createQueueAwaited(0.5, 1)
        }
        const startMonitorInQueue = theGroup.startMonitorInQueue
        if(!activeMonitor.isStarted)return;
        // e = monitor object
        clearTimeout(activeMonitor.resetFatalErrorCountTimer)
        activeMonitor.resetFatalErrorCountTimer = setTimeout(()=>{
            activeMonitor.errorFatalCount = 0
        },1000 * 60)
        s.sendMonitorStatus({
            id: monitorId,
            ke: groupKey,
            status: lang.Starting,
            code: 1
        });
        //create host string without username and password
        const strippedHost = stripAuthFromHost(e)
        async function doOnThisMachine(callback){
            await createCameraFolders(e)
            activeMonitor.allowStdinWrite = false
            setMotionLock(e)
            //start "no motion" checker
            if(e.details.detector === '1' && e.details.detector_notrigger === '1'){
                setNoEventsDetector(e)
            }
            if(config.childNodes.mode !== 'child' && s.platform!=='darwin' && (e.functionMode === 'record' || (e.functionMode === 'start'&&e.details.detector_record_method==='sip'))){
                if(activeMonitor.fswatch && activeMonitor.fswatch.close){
                  activeMonitor.fswatch.close()
                }
                activeMonitor.fswatch = fs.watch(e.dir, {encoding : 'utf8'}, (event, filename) => {
                    switch(event){
                        case'change':
                            resetRecordingCheck(e)
                        break;
                    }
                });
            }
            if(
                isMacOS &&
                isWatchOnlyOrRecord &&
                (streamTypeIsJPEG || streamTypeIsHLS || jpegApiEnabled)
            ){
                if(activeMonitor.fswatchStream && activeMonitor.fswatchStream.close){
                    activeMonitor.fswatchStream.close()
                }
                activeMonitor.fswatchStream = fs.watch(activeMonitor.sdir, {encoding : 'utf8'}, () => {
                    resetStreamCheck(e)
                })
            }
            if(!activeMonitor.criticalErrors['453'])s.cameraSendSnapshot({mid:monitorId,ke:groupKey,mon:e},{useIcon: true});
            //check host to see if has password and user in it
            clearTimeout(activeMonitor.recordingChecker)
            try{
                await cameraDestroy(e)
            }catch(err){
                // s.debugLog(err)
            }
            async function startVideoProcessor(err,pingResponse){
                pingResponse = pingResponse ? pingResponse : {success: true}
                return new Promise((resolve) => {
                    if(pingResponse.success === true){
                        activeMonitor.isRecording = true
                        try{
                            createCameraFfmpegProcess(e).then((mainProcess) => {
                                if(mainProcess){
                                    createEventCounter(e)
                                    createCameraStreamHandlers(e)
                                    if(typeIsDashcam){
                                        setTimeout(function(){
                                            activeMonitor.allowStdinWrite = true
                                            s.txToDashcamUsers({
                                                f : 'enable_stream',
                                                ke : groupKey,
                                                mid : monitorId
                                            },groupKey)
                                        },30000)
                                    }
                                    if(
                                        isRecord ||
                                        typeIsMjpeg ||
                                        typeIsH264 ||
                                        typeIsLocal
                                    ){
                                        catchNewSegmentNames(e)
                                        cameraFilterFfmpegLog(e)
                                    }
                                    if(isRecord){
                                        s.sendMonitorStatus({
                                            id: monitorId,
                                            ke: groupKey,
                                            status: lang.Recording,
                                            code: 3
                                        });
                                    }else{
                                        s.sendMonitorStatus({
                                            id: monitorId,
                                            ke: groupKey,
                                            status: lang.Watching,
                                            code: 2
                                        });
                                    }
                                }
                                s.onMonitorStartExtensions.forEach(function(extender){
                                    extender(Object.assign(theGroup.rawMonitorConfigurations[monitorId],{}),e)
                                })
                                resolve()
                            })
                        }catch(err){
                            console.log('Failed to Load',monitorId,groupKey)
                            console.log(err)
                            resolve()
                        }
                      }else{
                          s.onMonitorPingFailedExtensions.forEach(function(extender){
                              extender(Object.assign(theGroup.rawMonitorConfigurations[monitorId],{}),e)
                          })
                          s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1});
                          fatalError(e,"Ping Failed").then(() => {
                              resolve();
                          });
                      }
                })
            }
            if(doPingTest){
                try{
                    const testResult = await asyncConnectionTest(strippedHost,e.port,2000)
                    await startVideoProcessor(testResult.err,testResult.response)
                }catch(err){
                    await startVideoProcessor()
                }
            }else{
                await startVideoProcessor()
            }
            if(callback)callback()
        }
        async function doOnChildMachine(){
            function startVideoProcessor(){
                s.cx({
                    f : 'cameraStart',
                    mode : e.functionMode,
                    d : theGroup.rawMonitorConfigurations[monitorId]
                },activeMonitor.childNodeId)
                clearTimeout(activeMonitor.recordingChecker);
                clearTimeout(activeMonitor.streamChecker);
            }
            if(doPingTest){
                const testResult = await asyncConnectionTest(strippedHost,e.port,2000)
                startVideoProcessor(testResult.err,testResult.response)
            }else{
                startVideoProcessor()
            }
        }
        try{
            if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){
                const selectedNode = await selectNodeForOperation({
                    ke: groupKey,
                    mid: monitorId,
                });
                if(selectedNode){
                    bindMonitorToChildNode({
                        ke: groupKey,
                        mid: monitorId,
                        childNodeId: selectedNode,
                    })
                    await doOnChildMachine()
                }else{
                    startMonitorInQueue.push(doOnThisMachine,function(){})
                }
            }else{
                startMonitorInQueue.push(doOnThisMachine,function(){})
            }
        }catch(err){
            startMonitorInQueue.push(doOnThisMachine,function(){})
            console.log(err)
        }
    }
    async function fatalError(e,errorMessage){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        const monitorConfig = copyMonitorConfiguration(groupKey,monitorId)
        const monitorDetails = monitorConfig.details
        const maxCount = !monitorDetails.fatal_max || isNaN(monitorDetails.fatal_max) ? 0 : parseFloat(monitorDetails.fatal_max);
        clearTimeout(activeMonitor.fatalErrorTimeout);
        ++activeMonitor.errorFatalCount;
        if(activeMonitor.isStarted === true){
            activeMonitor.fatalErrorTimeout = setTimeout(function(){
                if(maxCount !== 0 && activeMonitor.errorFatalCount > maxCount){
                    s.userLog(e,{type:lang["Fatal Error"],msg:lang.onFatalErrorExit});
                    s.camera('stop',{mid:monitorId,ke:groupKey})
                }else{
                    launchMonitorProcesses(monitorConfig)
                };
            },5000);
        }else{
            await cameraDestroy(e)
        }
        s.sendMonitorStatus({
            id: monitorId,
            ke: groupKey,
            status: lang.Died,
            code: 7
        });
        s.onMonitorDiedExtensions.forEach(function(extender){
            extender(monitorConfig,e)
        })
    }
    async function monitorStart(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const monitorConfig = getMonitorConfiguration(groupKey,monitorId);
        monitorConfigurationMigrator(e)
        s.initiateMonitorObject({ke:groupKey,mid:monitorId})
        const activeMonitor = getActiveMonitor(groupKey,monitorId)
        if(!monitorConfig){
            s.group[groupKey].rawMonitorConfigurations[monitorId] = s.cleanMonitorObject(e)
        }
        if(activeMonitor.isStarted === true){
            s.debugLog('Monitor Already Started!')
            return
        }
        if(activeMonitor.masterSaysToStop === true){
            s.sendMonitorStatus({
                id: monitorId,
                ke: groupKey,
                status: lang.Stopped,
                code: 5,
            })
            return;
        }
        if(config.probeMonitorOnStart === true){
            const {
                configPartial,
                warnings,
                probeResponse,
                probeStreams,
            } = await getWarningChangesForMonitor(monitorConfig)
            if(warnings.length > 0){
                applyPartialToConfiguration(e,configPartial)
                applyPartialToConfiguration(activeMonitor,configPartial)
                applyPartialToConfiguration(s.group[groupKey].rawMonitorConfigurations[monitorId],configPartial)
            }
            activeMonitor.warnings = warnings
        }
        activeMonitor.isStarted = true
        if(e.details && e.details.dir && e.details.dir !== ''){
            activeMonitor.addStorageId = e.details.dir
        }else{
            activeMonitor.addStorageId = null
        }
        //set recording status
        if(e.functionMode === 'record'){
            activeMonitor.isRecording = true
        }else{
            activeMonitor.isRecording = false
        }
        //set up fatal error handler
        activeMonitor.errorFatalCount = 0;
        delete(activeMonitor.childNode)
        if(e.details.detector_ptz_follow === '1'){
            setHomePositionPreset(e)
        }
        try{
            await launchMonitorProcesses(e)
            resetStreamCheck(e)
        }catch(err){
            console.error(err)
        }
    }
    function checkObjectsInMonitorDetails(e){
        const groupKey = e.ke
        const monitorId = e.mid || e.id
        const activeMonitor = getActiveMonitor(groupKey,monitorId);
        //parse Objects
        (['detector_cascades','cords','detector_filters','input_map_choices']).forEach(function(v){
            if(e.details && e.details[v]){
                try{
                    if(!e.details[v] || e.details[v] === '')e.details[v] = '{}'
                    e.details[v] = s.parseJSON(e.details[v])
                    if(!e.details[v])e.details[v] = {}
                    activeMonitor.details = e.details
                    switch(v){
                        case'cords':
                            const fromWidth = parseInt(e.details.detector_scale_x) || 640
                            const fromHeight = parseInt(e.details.detector_scale_y) || 480
                            const toWidth = parseInt(e.details.detector_scale_x_object) || 1280
                            const toHeight = parseInt(e.details.detector_scale_y_object) || 720
                            const theCords = Object.values(s.parseJSON(e.details[v])) || [];
                            activeMonitor.parsedObjects.cordsForObjectDetection = convertRegionPointsToNewDimensions(theCords,{
                                fromWidth,
                                fromHeight,
                                toWidth,
                                toHeight,
                            });
                            activeMonitor.parsedObjects.cords = theCords
                        break;
                        default:
                            activeMonitor.parsedObjects[v] = s.parseJSON(e.details[v])
                        break;
                    }
                }catch(err){

                }
            }
        });
        //parse Arrays
        (['stream_channels','input_maps']).forEach(function(v){
            if(e.details&&e.details[v]&&(e.details[v] instanceof Array)===false){
                try{
                    e.details[v]=JSON.parse(e.details[v]);
                    if(!e.details[v])e.details[v]=[];
                }catch(err){
                    e.details[v]=[];
                }
            }
        });
    }
    function isGroupBelowMaxMonitorCount(groupKey){
        const theGroup = s.group[groupKey];
        try{
            const initData = theGroup.init;
            const maxCamerasAllowed = parseInt(initData.max_camera) || false;
            return (!maxCamerasAllowed || Object.keys(theGroup.activeMonitors).length <= parseInt(maxCamerasAllowed))
        }catch(err){
            return true
        }
    }
    function getStreamDirectory(options){
        const streamDir = s.dir.streams + options.ke + '/' + options.mid + '/'
        return streamDir
    }
    return {
        monitorStop,
        monitorIdle,
        monitorStart,
        monitorRestart,
        monitorAddViewer,
        monitorRemoveViewer,
        getUrlProtocol,
        modifyUrlProtocol,
        getUrlParts,
        deleteMonitor,
        deleteMonitorData,
        checkObjectsInMonitorDetails,
        getActiveMonitor,
        getStreamDirectory,
        copyMonitorConfiguration,
        getMonitorConfiguration,
        isGroupBelowMaxMonitorCount,
        setNoEventsDetector,
        cameraDestroy: cameraDestroy,
        createSnapshot: createSnapshot,
        processKill: processKill,
        addCredentialsToStreamLink: addCredentialsToStreamLink,
        monitorConfigurationMigrator: monitorConfigurationMigrator,
        spawnSubstreamProcess: spawnSubstreamProcess,
        destroySubstreamProcess: destroySubstreamProcess,
        attachStreamChannelHandlers: attachStreamChannelHandlers,
        setActiveViewer: setActiveViewer,
        getActiveViewerCount: getActiveViewerCount,
        setTimedActiveViewerForHttp: setTimedActiveViewerForHttp,
        attachMainProcessHandlers: attachMainProcessHandlers,
    }
}
