Skip to content

Commit 48d3104

Browse files
Add keyboard shortcuts and support milliseconds
1 parent 02bc587 commit 48d3104

File tree

7 files changed

+199
-87
lines changed

7 files changed

+199
-87
lines changed

fcut.lua

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ local tables = require('jls.util.tables')
1313
local strings = require('jls.util.strings')
1414
local Map = require('jls.util.Map')
1515
local List = require('jls.util.List')
16+
local HttpContext = require('jls.net.http.HttpContext')
1617
local HttpExchange = require('jls.net.http.HttpExchange')
1718
local FileHttpHandler = require('jls.net.http.handler.FileHttpHandler')
1819
local RestHttpHandler = require('jls.net.http.handler.RestHttpHandler')
@@ -62,6 +63,9 @@ local function startProcess(command, outputFile, callback)
6263
fd = FileDescriptor.openSync(outputFile, 'w')
6364
pb:redirectOutput(fd)
6465
end
66+
if logger:isLoggable(logger.FINE) then
67+
logger:fine('Start process: '..table.concat(command, ' '))
68+
end
6569
local ph = pb:start()
6670
ph:ended():next(function(exitCode)
6771
if fd then
@@ -96,15 +100,9 @@ end
96100

97101
local scriptFile = File:new(arg[0]):getAbsoluteFile()
98102
local scriptDir = scriptFile:getParentFile()
99-
100-
local assetsHandler
101103
local assetsDir = File:new(scriptDir, 'assets')
102104
local assetsZip = File:new(scriptDir, 'assets.zip')
103-
if assetsDir:isDirectory() then
104-
assetsHandler = FileHttpHandler:new(assetsDir)
105-
elseif assetsZip:isFile() then
106-
assetsHandler = ZipFileHttpHandler:new(assetsZip)
107-
end
105+
local extensionsDir = File:new(scriptDir, config.extensions)
108106

109107
local commandQueue = {}
110108
local commandCount = 0
@@ -133,6 +131,28 @@ ffmpeg:configure(config)
133131

134132
-- Application local functions
135133

134+
local function loadExtensions(...)
135+
if not extensionsDir:isDirectory() then
136+
return
137+
end
138+
logger:info('Loading extensions from directory "'..extensionsDir:getPath()..'"')
139+
for _, extensionDir in ipairs(extensionsDir:listFiles()) do
140+
local extensionLuaFile = File:new(extensionDir, 'init.lua')
141+
if extensionDir:isDirectory() and extensionLuaFile:isFile() then
142+
local scriptFn, err = loadfile(extensionLuaFile:getPath())
143+
if not scriptFn or err then
144+
logger:warn('Cannot load extension from script "'..extensionLuaFile:getPath()..'" due to '..tostring(err))
145+
else
146+
local status
147+
status, err = pcall(scriptFn, ...)
148+
if not status then
149+
logger:warn('Cannot load extension from script "'..extensionLuaFile:getPath()..'" due to '..tostring(err))
150+
end
151+
end
152+
end
153+
end
154+
end
155+
136156
local function terminate()
137157
for _, ph in ipairs(processList) do
138158
ph:destroy()
@@ -188,18 +208,50 @@ local function enqueueCommand(args, id, outputFile)
188208
return promise
189209
end
190210

211+
local function endExport(exportContext, webSocket, exitCode)
212+
exportContext.exitCode = exitCode
213+
webSocket:close()
214+
Map.deleteValues(exportContexts, exportContext)
215+
end
216+
217+
local function startExportCommand(exportContext, webSocket, index)
218+
local command = exportContext.commands[index]
219+
if not command then
220+
endExport(exportContext, webSocket, 0)
221+
return
222+
end
223+
webSocket:sendTextMessage('\n -- starting command '..tostring(index)..'/'..tostring(#exportContext.commands)..' ------\n\n')
224+
local pb = createProcessBuilder(command)
225+
local p = Pipe:new()
226+
pb:redirectError(p)
227+
local ph = pb:start()
228+
ph:ended():next(function(exitCode)
229+
if exitCode == 0 then
230+
startExportCommand(exportContext, webSocket, index + 1)
231+
else
232+
endExport(exportContext, webSocket, exitCode)
233+
end
234+
end)
235+
exportContext.process = ph
236+
p:readStart(function(err, data)
237+
if not err and data then
238+
webSocket:sendTextMessage(data)
239+
else
240+
p:close()
241+
end
242+
end)
243+
end
244+
191245
-- HTTP contexts used by the web application
192246
local httpContexts = {
247+
-- HTTP resources
193248
['/(.*)'] = FileHttpHandler:new(File:new(scriptDir, 'htdocs'), nil, 'fcut.html'),
249+
-- Context to retrieve the configuration
194250
['/config/(.*)'] = TableHttpHandler:new(config, nil, true),
195-
['/assets/(.*)'] = assetsHandler,
196-
}
197-
198-
-- Create the HTTP contexts used by the web application
199-
local function createHttpContexts(httpServer)
200-
logger:info('HTTP Server bound on port '..tostring(select(2, httpServer:getAddress())))
251+
-- Assets HTTP resources directory or ZIP file
252+
['/assets/(.*)'] = assetsZip:isFile() and not assetsDir:isDirectory() and ZipFileHttpHandler:new(assetsZip) or FileHttpHandler:new(assetsDir),
201253
-- Context to retrieve and cache a movie image at a specific time
202-
httpServer:createContext('/source/([^/]+)/(%d+)%.jpg', Map.assign(FileHttpHandler:new(cacheDir), {
254+
['/source/([^/]+)/(%d+%.?%d*)%.jpg'] = Map.assign(FileHttpHandler:new(cacheDir), {
203255
getPath = function(_, exchange)
204256
return string.sub(exchange:getRequest():getTargetPath(), 9)
205257
end,
@@ -210,9 +262,9 @@ local function createHttpContexts(httpServer)
210262
return enqueueCommand(command, 'preview')
211263
end)
212264
end
213-
}))
265+
}),
214266
-- Context to retrieve and cache a movie information
215-
httpServer:createContext('/source/([^/]+)/info%.json', Map.assign(FileHttpHandler:new(cacheDir), {
267+
['/source/([^/]+)/info%.json'] = Map.assign(FileHttpHandler:new(cacheDir), {
216268
getPath = function(_, exchange)
217269
return string.sub(exchange:getRequest():getTargetPath(), 9)
218270
end,
@@ -223,9 +275,9 @@ local function createHttpContexts(httpServer)
223275
return enqueueCommand(command, nil, tmpFile)
224276
end)
225277
end
226-
}))
278+
}),
227279
-- Context for the application REST API
228-
httpServer:createContext('/rest/(.*)', RestHttpHandler:new({
280+
['/rest/(.*)'] = RestHttpHandler:new({
229281
['getSourceId?method=POST'] = function(exchange)
230282
local filename = exchange:getRequest():getBody()
231283
return ffmpeg:openSource(filename)
@@ -288,70 +340,33 @@ local function createHttpContexts(httpServer)
288340
}
289341
return exportId
290342
end,
291-
}))
343+
}),
292344
-- Context that handle the export commands and output to a WebSocket
293-
httpServer:createContext('/console/(.*)', Map.assign(WebSocketUpgradeHandler:new(), {
345+
['/console/(.*)'] = Map.assign(WebSocketUpgradeHandler:new(), {
294346
onOpen = function(_, webSocket, exchange)
295347
local exportId = exchange:getRequestArguments()
296348
local exportContext = exportContexts[exportId]
297349
if not exportContext then
298350
webSocket:close()
299351
return
300352
end
301-
local commands = exportContext.commands
302353
local header = {' -- export commands ------'};
303-
for index, command in ipairs(commands) do
354+
for index, command in ipairs(exportContext.commands) do
304355
table.insert(header, ' '..tostring(index)..': '..table.concat(command, ' '))
305356
end
306357
table.insert(header, '')
307358
webSocket:sendTextMessage(table.concat(header, '\n'))
308-
local function endExport(exitCode)
309-
exportContext.exitCode = exitCode
310-
webSocket:close()
311-
exportContexts[exportId] = nil
312-
end
313-
local index = 0
314-
local function startNextCommand()
315-
index = index + 1
316-
if index > #commands then
317-
endExport(0)
318-
return
319-
end
320-
local command = commands[index]
321-
webSocket:sendTextMessage('\n -- starting command '..tostring(index)..'/'..tostring(#commands)..' ------\n\n')
322-
local pb = createProcessBuilder(command)
323-
local p = Pipe:new()
324-
pb:redirectError(p)
325-
local ph = pb:start()
326-
ph:ended():next(function(exitCode)
327-
if exitCode == 0 then
328-
startNextCommand()
329-
else
330-
endExport(exitCode)
331-
end
332-
end)
333-
exportContext.process = ph
334-
p:readStart(function(err, data)
335-
if not err and data then
336-
webSocket:sendTextMessage(data)
337-
else
338-
p:close()
339-
end
340-
end)
341-
end
342-
startNextCommand()
359+
startExportCommand(exportContext, webSocket, 1)
343360
end
344-
}))
345-
end
361+
}),
362+
HttpContext:new('/extensions/([a-zA-Z0-9%-_]+)/(.*)', FileHttpHandler:new(extensionsDir, 'r', 'index.html')):setPathReplacement('%1/htdocs/%2'),
363+
}
346364

347365
-- Start the application as an HTTP server or a WebView
348366
if config.webview.disable then
349367
local httpServer = require('jls.net.http.HttpServer'):new()
350368
httpServer:bind(config.webview.address, config.webview.port):next(function()
351-
for path, handler in pairs(httpContexts) do
352-
httpServer:createContext(path, handler)
353-
end
354-
createHttpContexts(httpServer)
369+
httpServer:createContexts(httpContexts)
355370
if config.webview.port == 0 then
356371
print('FCut HTTP Server available at http://localhost:'..tostring(select(2, httpServer:getAddress())))
357372
end
@@ -363,20 +378,24 @@ if config.webview.disable then
363378
--HttpExchange.ok(exchange, 'Closing')
364379
end,
365380
}))
381+
loadExtensions(httpServer)
366382
end, function(err)
367383
logger:warn('Cannot bind HTTP server due to '..tostring(err))
368384
os.exit(1)
369385
end)
370386
else
371-
require('jls.util.WebView').open('http://localhost:'..tostring(config.webview.port)..'/', {
387+
local url = 'http://localhost:'..tostring(config.webview.port)..'/'
388+
if config.extension then
389+
url = url..'extensions/'..config.extension..'/'
390+
end
391+
require('jls.util.WebView').open(url, {
372392
title = 'Fast Cut (Preview)',
373393
resizable = true,
374394
bind = true,
375395
debug = config.webview.debug,
376396
contexts = httpContexts,
377397
}):next(function(webview)
378398
local httpServer = webview:getHttpServer()
379-
createHttpContexts(httpServer)
380399
httpServer:createContext('/webview/(.*)', RestHttpHandler:new({
381400
['fullscreen(requestJson)?method=POST&Content-Type=application/json'] = function(exchange, fullscreen)
382401
webview:fullscreen(fullscreen == true);
@@ -417,6 +436,7 @@ else
417436
return self:call(getFileName, order)
418437
end
419438
end
439+
loadExtensions(httpServer)
420440
return webview:getThread():ended()
421441
end):next(function()
422442
logger:info('WebView closed')

fcutSchema.lua

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ return {
2727
title = 'A project file to load',
2828
type = 'string',
2929
},
30+
source = {
31+
title = 'A source file to add',
32+
type = 'string',
33+
},
34+
extension = {
35+
title = 'The extensions path',
36+
type = 'string',
37+
},
38+
extensions = {
39+
title = 'The extension to open at startup',
40+
type = 'string',
41+
default = 'extensions',
42+
},
3043
processCount = {
3144
title = 'The maximum number of running processes',
3245
type = 'integer',
@@ -40,7 +53,7 @@ return {
4053
default = (require('jls.lang.system').isWindows() and 'ffmpeg\\ffmpeg.exe' or '/usr/bin/ffmpeg'),
4154
},
4255
ffprobe = {
43-
title = 'The ffprobe path',
56+
title = 'The ffprobe path, the default value is computed from the ffmpeg path',
4457
type = 'string',
4558
},
4659
loglevel = {

htdocs/FileChooser.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
template: '<div class="file-chooser-dialog">' +
4545
'<div class="file-chooser-flex-row">' +
4646
' <input type="text" v-model="inputPath" v-on:keyup="setPath(inputPath)" class="file-chooser-flex-row-content" />' +
47-
' <button v-on:click="showSettings = !showSettings">&#x2699;</button>' +
47+
' <button v-on:click="showSettings = !showSettings" v-bind:class="{pressed: showSettings}"><i class="fas fa-filter"></i></button>' +
4848
'</div>' +
4949
'<div class="file-chooser-content" style="overflow: auto;">' +
5050
' <ul>' +
@@ -55,7 +55,7 @@
5555
' </ul>' +
5656
'</div>' +
5757
'<div v-if="showSettings" class="file-chooser-flex-row">' +
58-
' <button v-on:click="refresh()">Refresh</button>' +
58+
' <button v-on:click="refresh()"><i class="fas fa-redo"></i></button>' +
5959
' <span>Filter:</span>' +
6060
' <input type="text" v-model="extention" class="file-chooser-flex-row-content" />' +
6161
' <input type="checkbox" v-model="showAll" />' +

htdocs/fcut-utils.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,26 @@ function showMessage() {
8585
});
8686
}
8787

88+
function withoutExtension(filename) {
89+
if (typeof filename !== 'string') {
90+
return filename;
91+
}
92+
return filename.replace(/\.[^/\\.]+$/, '');
93+
}
94+
95+
function basename(filename) {
96+
if (typeof filename !== 'string') {
97+
return filename;
98+
}
99+
return filename.replace(/^.*[/\\]([^/\\]+)$/, '$1');
100+
}
101+
102+
function dirname(filename) {
103+
if (typeof filename !== 'string') {
104+
return filename;
105+
}
106+
return filename.replace(/[/\\][^/\\]+$/, '');
107+
}
88108

89109
var fileChooserCallback = null;
90110

@@ -107,11 +127,12 @@ function onFileChoosed(names) {
107127
}
108128
}
109129

110-
function chooseFiles(fileChooser, multiple, save, path, extention) {
130+
function chooseFiles(fileChooser, multiple, save, path, extention, name) {
111131
fileChooser.multiple = multiple === true;
112132
fileChooser.save = save === true;
113133
fileChooser.label = save ? 'Save' : 'Open';
114134
fileChooser.extention = extention || '';
135+
fileChooser.name = name || '';
115136
if (path) {
116137
fileChooser.list(path);
117138
} else {

0 commit comments

Comments
 (0)