Method to merge videos from Videos List

- button will appear as "Merge and Download" next to "Zip and Download"
merge-requests/63/head
Moe 2019-01-28 17:41:14 -08:00
parent bbb6ef108a
commit 75269f7d9f
6 changed files with 185 additions and 34 deletions

View File

@ -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",

View File

@ -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){

View File

@ -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
}
}

View File

@ -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'])
}

View File

@ -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('<iframe>a</iframe>').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+'<div style="margin-bottom:15px"></div>'
$.each(videos,function(n,v){
html+=v.filename+'<br>';
})
$.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('<iframe>a</iframe>').find('iframe').attr('src',downloadZip)
});
})
$.vidview.pages.on('click','[page]',function(e){
e.limit=$.vidview.limit.val();
e.page=$(this).attr('page');

View File

@ -43,6 +43,7 @@
<div class="col-md-4 text-left">
<a class="btn btn-danger delete_selected"><i class="fa fa-trash-o"></i> &nbsp; <%-lang['Delete']%></a>
<a class="btn btn-default export_selected"><i class="fa fa-folder-o"></i> &nbsp; <%-lang['Zip and Download']%></a>
<a class="btn btn-default merge_selected"><i class="fa fa-copy"></i> &nbsp; <%-lang['Merge and Download']%></a>
</div>
<div class="col-md-4">
<div class="text-center" id="videos_viewer_pages"></div>