From 75269f7d9fe0d03d48276cef43185ffbc8e261ff Mon Sep 17 00:00:00 2001 From: Moe Date: Mon, 28 Jan 2019 17:41:14 -0800 Subject: [PATCH] Method to merge videos from Videos List - button will appear as "Merge and Download" next to "Zip and Download" --- languages/en_CA.json | 5 +++ libs/monitor.js | 66 ++++++++++++++++++++++++++++ libs/videos.js | 36 +++++++++++++++ libs/webServerPaths.js | 80 ++++++++++++++++++++-------------- web/libs/js/dash2.vidview.js | 31 ++++++++++++- web/pages/blocks/videoview.ejs | 1 + 6 files changed, 185 insertions(+), 34 deletions(-) diff --git a/languages/en_CA.json b/languages/en_CA.json index 169ba570..26b65ebe 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -261,13 +261,17 @@ "Set to Watch Only": "Set to Watch Only", "Save as": "Save as", "Add New": "Add New", + "Merge and Download": "Merge and Download", "Zip and Download": "Zip and Download", + "Merge Selected Videos": "Merge Selected Videos", "Export Selected Videos": "Export Selected Videos", "Delete Selected Videos": "Delete Selected Videos", "DeleteSelectedVideosMsg": "Do you want to delete these videos? You cannot recover them.", "ExportSelectedVideosMsg": "Do you want to export these videos? It may take some time to zip and download.", + "MergeSelectedVideosMsg": "Do you want to merge these videos? It may take some time to merge and download. The moment the connection is closed the file will be deleted. Ensure you keep the browser open until it is complete.", "clientStreamFailedattemptingReconnect": "Client side ctream check failed, attempting reconnect.", "Export Video": "Export Video", + "Merge Video": "Merge Video", "Delete Filter": "Delete Filter", "confirmDeleteFilter": "Do you want to delete this filter? You cannot recover it.", "Fix Video": "Fix Video", @@ -716,6 +720,7 @@ "Preview":"Preview", "Websocket Connected":"Websocket Connected", "Websocket Disconnected":"Websocket Disconnected", + "Videos Merge":"Videos Merge", "Token":"Token", "Channel ID":"Channel ID", "New Authentication Token":"New Authentication Token", diff --git a/libs/monitor.js b/libs/monitor.js index 8d8dc92c..ad9176b5 100644 --- a/libs/monitor.js +++ b/libs/monitor.js @@ -223,6 +223,64 @@ module.exports = function(s,config,lang){ }) return items } + s.mergeRecordedVideos = function(videoRows,groupKey,callback){ + var tempDir = s.dir.streams + groupKey + '/' + var pathDir = s.dir.fileBin + groupKey + '/' + var streamDirItems = fs.readdirSync(pathDir) + var items = [] + var mergedFile = [] + videoRows.forEach(function(video){ + var filepath = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext + if( + filepath.indexOf('.mp4') > -1 + // || filename.indexOf('.webm') > -1 + ){ + mergedFile.push(s.formattedTime(video.time)) + items.push(filepath) + } + }) + mergedFile.sort() + mergedFile = mergedFile.join('_') + '.mp4' + var mergedFilepath = pathDir + mergedFile + var mergedRawFilepath = pathDir + 'raw_' + mergedFile + items.sort() + fs.stat(mergedFilepath,function(err,stats){ + if(err){ + //not exist + var tempScriptPath = tempDir + s.gid(5) + '.sh' + var cat = 'cat '+items.join(' ')+' > '+mergedRawFilepath + fs.writeFileSync(tempScriptPath,cat,'utf8') + exec('sh ' + tempScriptPath,function(){ + s.userLog({ + ke: groupKey, + mid: '$USER' + },{type:lang['Videos Merge'],msg:mergedFile}) + var merger = spawn(config.ffmpegDir,s.splitForFFPMEG(('-re -loglevel warning -i ' + mergedRawFilepath + ' -acodec copy -vcodec copy ' + mergedFilepath))) + merger.stderr.on('data',function(data){ + s.userLog({ + ke: groupKey, + mid: '$USER' + },{type:lang['Videos Merge'],msg:data.toString()}) + }) + merger.on('close',function(){ + s.file('delete',mergedRawFilepath) + s.file('delete',tempScriptPath) + setTimeout(function(){ + fs.stat(mergedFilepath,function(err,stats){ + if(!err)s.file('delete',mergedFilepath) + }) + },1000 * 60 * 60 * 24) + delete(merger) + callback(mergedFilepath,mergedFile) + }) + }) + }else{ + //file exist + callback(mergedFilepath,mergedFile) + } + }) + return items + } s.cameraDestroy = function(x,e,p){ if(s.group[e.ke]&&s.group[e.ke].mon[e.id]&&s.group[e.ke].mon[e.id].spawn !== undefined){ @@ -609,6 +667,14 @@ module.exports = function(s,config,lang){ // exec('chmod -R 777 '+e.sdir,function(err){ // // }) + var binDir = s.dir.fileBin + e.ke + '/' + if (!fs.existsSync(binDir)){ + fs.mkdirSync(binDir) + } + binDir = s.dir.fileBin + e.ke + '/' + e.id + '/' + if (!fs.existsSync(binDir)){ + fs.mkdirSync(binDir) + } return setStreamDir } s.stripAuthFromHost = function(e){ diff --git a/libs/videos.js b/libs/videos.js index ae23edb0..4d617568 100644 --- a/libs/videos.js +++ b/libs/videos.js @@ -373,4 +373,40 @@ module.exports = function(s,config,lang){ finish() } } + s.streamMp4FileOverHttp = function(filePath,req,res){ + var ext = filePath.split('.') + ext = filePath[filePath.length - 1] + var total = fs.statSync(filePath).size; + if (req.headers['range']) { + try{ + var range = req.headers.range; + var parts = range.replace(/bytes=/, "").split("-"); + var partialstart = parts[0]; + var partialend = parts[1]; + var start = parseInt(partialstart, 10); + var end = partialend ? parseInt(partialend, 10) : total-1; + var chunksize = (end-start)+1; + var file = fs.createReadStream(filePath, {start: start, end: end}); + req.headerWrite={ 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/'+req.ext } + req.writeCode=206 + }catch(err){ + req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+req.ext}; + var file = fs.createReadStream(filePath) + req.writeCode=200 + } + } else { + req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+req.ext}; + var file = fs.createReadStream(filePath) + req.writeCode=200 + } + if(req.query.downloadName){ + req.headerWrite['content-disposition']='attachment; filename="'+req.query.downloadName+'"'; + } + res.writeHead(req.writeCode,req.headerWrite); + file.on('close',function(){ + res.end() + }) + file.pipe(res) + return file + } } diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index 47e26364..7ce5e8a4 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -926,6 +926,53 @@ module.exports = function(s,config,lang,app,io){ s.auth(req.params,req.fn,res,req); }); /** + * API : Merge Recorded Videos into one file + */ + app.get(config.webPaths.apiPrefix+':auth/videosMerge/:ke', function (req,res){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + var failed = function(resp){ + res.setHeader('Content-Type', 'application/json'); + res.end(s.prettyPrint(resp)) + } + if(req.query.videos && req.query.videos !== ''){ + s.auth(req.params,function(user){ + var videosSelected = JSON.parse(req.query.videos) + var where = [] + var values = [] + videosSelected.forEach(function(video){ + where.push("(ke=? AND mid=? AND `time`=?)") + if(!video.ke)video.ke = req.params.ke + values.push(video.ke) + values.push(video.mid) + var time = s.nameToTime(video.filename) + if(req.query.isUTC === 'true'){ + time = s.utcToLocal(time) + } + time = new Date(time) + values.push(time) + }) + s.sqlQuery('SELECT * FROM Videos WHERE '+where.join(' OR '),values,function(err,r){ + var resp = {ok: false} + if(r && r[0]){ + s.mergeRecordedVideos(r,req.params.ke,function(fullPath,filename){ + res.setHeader('Content-Disposition', 'attachment; filename="'+filename+'"') + var file = fs.createReadStream(fullPath) + file.on('close',function(){ + s.file('delete',fullPath) + res.end() + }) + file.pipe(res) + }) + }else{ + failed({ok:false,msg:'No Videos Found'}) + } + }) + },res,req); + }else{ + failed({ok:false,msg:'"videos" query variable is missing from request.'}) + } + }) + /** * API : Get Videos */ app.get([ @@ -1628,38 +1675,7 @@ module.exports = function(s,config,lang,app,io){ if(r&&r[0]){ req.dir=s.getVideoDirectory(r[0])+req.params.file if (fs.existsSync(req.dir)){ - req.ext=req.params.file.split('.')[1]; - var total = fs.statSync(req.dir).size; - if (req.headers['range']) { - try{ - var range = req.headers.range; - var parts = range.replace(/bytes=/, "").split("-"); - var partialstart = parts[0]; - var partialend = parts[1]; - var start = parseInt(partialstart, 10); - var end = partialend ? parseInt(partialend, 10) : total-1; - var chunksize = (end-start)+1; - var file = fs.createReadStream(req.dir, {start: start, end: end}); - req.headerWrite={ 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/'+req.ext } - req.writeCode=206 - }catch(err){ - req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+req.ext}; - var file = fs.createReadStream(req.dir) - req.writeCode=200 - } - } else { - req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+req.ext}; - var file=fs.createReadStream(req.dir) - req.writeCode=200 - } - if(req.query.downloadName){ - req.headerWrite['content-disposition']='attachment; filename="'+req.query.downloadName+'"'; - } - res.writeHead(req.writeCode,req.headerWrite); - file.on('close',function(){ - res.end(); - }) - file.pipe(res); + s.streamMp4FileOverHttp(req.dir,req,res) }else{ res.end(user.lang['File Not Found in Filesystem']) } diff --git a/web/libs/js/dash2.vidview.js b/web/libs/js/dash2.vidview.js index 9eb35383..aa754763 100644 --- a/web/libs/js/dash2.vidview.js +++ b/web/libs/js/dash2.vidview.js @@ -11,7 +11,7 @@ $.vidview={ $.vidview.set.change(function(){ var el = $(this) var isCloud = (el.val() === 'cloud') - var zipDlButton = $.vidview.e.find('.export_selected') + var zipDlButton = $.vidview.e.find('.export_selected,.merge_selected') if(isCloud){ zipDlButton.hide() }else{ @@ -119,11 +119,38 @@ $.vidview.e.find('.export_selected').click(function(){ if($.ccio.useUTC === true){ queryVariables.push('isUTC=true') } - console.log(queryVariables) var downloadZip = $.ccio.init('location',$user)+$user.auth_token+'/zipVideos/'+$user.ke+'?'+queryVariables.join('&') $('#temp').html('').find('iframe').attr('src',downloadZip); }); }) +$.vidview.e.find('.merge_selected').click(function(){ + e = {} + var videos = $.vidview.getSelected(true) + if(videos.length === 0){ + $.ccio.init('note',{ + title:'No Videos Selected', + text:'You must choose at least one video.', + type:'error' + },$user); + return + } + $.confirm.e.modal('show'); + $.confirm.title.text(lang['Merge Selected Videos']) + var html = lang.MergeSelectedVideosMsg+'
' + $.each(videos,function(n,v){ + html+=v.filename+'
'; + }) + $.confirm.body.html(html) + $.confirm.click({title:'Merge Video',class:'btn-danger'},function(){ + var queryVariables = [] + queryVariables.push('videos='+JSON.stringify(videos)) + if($.ccio.useUTC === true){ + queryVariables.push('isUTC=true') + } + var downloadZip = $.ccio.init('location',$user)+$user.auth_token+'/videosMerge/'+$user.ke+'?'+queryVariables.join('&') + $('#temp').html('').find('iframe').attr('src',downloadZip) + }); +}) $.vidview.pages.on('click','[page]',function(e){ e.limit=$.vidview.limit.val(); e.page=$(this).attr('page'); diff --git a/web/pages/blocks/videoview.ejs b/web/pages/blocks/videoview.ejs index 4113316f..65db05a7 100644 --- a/web/pages/blocks/videoview.ejs +++ b/web/pages/blocks/videoview.ejs @@ -43,6 +43,7 @@
  <%-lang['Delete']%>   <%-lang['Zip and Download']%> +   <%-lang['Merge and Download']%>