Skip to content

Commit 6f0e9e8

Browse files
first commit
0 parents  commit 6f0e9e8

File tree

13 files changed

+1584
-0
lines changed

13 files changed

+1584
-0
lines changed

ExecWorker.lua

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
local class = require('jls.lang.class')
2+
local Promise = require('jls.lang.Promise')
3+
local Worker = require('jls.util.Worker')
4+
local Map = require('jls.util.Map')
5+
local TableList = require('jls.util.TableList')
6+
local system = require('jls.lang.system')
7+
8+
local nullFilename = system.isWindows() and 'NUL' or '/dev/null'
9+
10+
return class.create(function(execWorker)
11+
12+
function execWorker:initialize()
13+
self.works = {}
14+
self.working = false
15+
self.worker = Map.assign(Worker:new(function(w)
16+
function w:onMessage(message)
17+
local status, kind, code
18+
if message.pipe then
19+
local f = io.popen(message.command)
20+
if f then
21+
self:postMessage('Process started, '..tostring(message.command))
22+
repeat
23+
local d = f:read(message.pipe)
24+
self:postMessage(d)
25+
until d == nil
26+
status, kind, code = f:close()
27+
if not status then
28+
self:postMessage('Process terminated with exit code '..tostring(code))
29+
end
30+
else
31+
status, kind, code = false, 'popen', 0
32+
end
33+
else
34+
status, kind, code = os.execute(message.command)
35+
end
36+
return self:postMessage({
37+
status = status,
38+
kind = kind,
39+
code = code,
40+
})
41+
end
42+
end), {
43+
onMessage = function(_, message)
44+
local work
45+
if type(message) == 'string' or message == nil then
46+
work = self.works[1]
47+
if work.sh then
48+
work.sh(message)
49+
end
50+
return
51+
elseif type(message) == 'table' then
52+
work = table.remove(self.works, 1)
53+
self.working = false
54+
if message.status then
55+
work.cb()
56+
else
57+
work.cb(message.code or 0)
58+
end
59+
self:wakeup()
60+
else
61+
error('Bad message '..tostring(message))
62+
end
63+
end
64+
})
65+
end
66+
67+
function execWorker:wakeup()
68+
if not self.working then
69+
local work = self.works[1]
70+
if work then
71+
self.working = true
72+
self.worker:postMessage({
73+
command = work.command,
74+
pipe = work.sh and (work.rf or 'L'),
75+
})
76+
end
77+
end
78+
end
79+
80+
function execWorker:execute(command, id, sh, redirect, rf)
81+
local suffix = ''
82+
if redirect == 'both' then
83+
suffix = ' 2>&1'
84+
elseif redirect == 'error' then
85+
suffix = ' 2>&1 1>'..nullFilename
86+
elseif redirect == 'output' then
87+
suffix = ' 2>'..nullFilename
88+
end
89+
local promise, cb = Promise.createWithCallback()
90+
local work = {
91+
command = command..suffix,
92+
cb = cb,
93+
id = id,
94+
sh = sh,
95+
redirect = redirect,
96+
rf = rf,
97+
}
98+
if id then
99+
TableList.removeIf(self.works, function(w, i)
100+
if i > 1 and w.id == id then
101+
w.cb('Cancelled')
102+
return true
103+
end
104+
return false
105+
end)
106+
end
107+
table.insert(self.works, work)
108+
self:wakeup()
109+
return promise
110+
end
111+
112+
function execWorker:close()
113+
self.worker:close()
114+
end
115+
116+
end)

Ffmpeg.lua

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
local class = require('jls.lang.class')
2+
local system = require('jls.lang.system')
3+
local runtime = require('jls.lang.runtime')
4+
local Promise = require('jls.lang.Promise')
5+
local StringBuffer = require('jls.lang.StringBuffer')
6+
local File = require('jls.io.File')
7+
local strings = require('jls.util.strings')
8+
local TableList = require('jls.util.TableList')
9+
local LocalDateTime = require('jls.util.LocalDateTime')
10+
11+
local function getExecutableName(name)
12+
if system.isWindows() then
13+
return name..'.exe'
14+
end
15+
return name
16+
end
17+
18+
local function getUserDir()
19+
if system.isWindows() then
20+
return os.getenv('TEMP') or os.getenv('USERPROFILE')
21+
end
22+
return os.getenv('HOME')
23+
end
24+
25+
local function hash(value)
26+
return math.abs(strings.hash(value))
27+
end
28+
29+
local function computeFileId(file)
30+
return ''
31+
..strings.formatInteger(hash(file:getName()), 64)
32+
--..strings.formatInteger(hash(file:getParent()) % MAX_ID_PART, 64)
33+
..strings.formatInteger(file:lastModified(), 64)
34+
..strings.formatInteger(file:length(), 64)
35+
end
36+
37+
38+
return class.create(function(ffmpeg)
39+
40+
function ffmpeg:initialize(options, execWorker)
41+
local cacheDir = File:new(options.cache)
42+
if not cacheDir:isAbsolute() then
43+
local homeDir = File:new(getUserDir() or '.')
44+
if not homeDir:isDirectory() then
45+
error('Invalid user directory, '..homeDir:getPath())
46+
end
47+
cacheDir = File:new(homeDir, options.cache):getAbsoluteFile()
48+
end
49+
if not cacheDir:isDirectory() then
50+
if not cacheDir:mkdir() then
51+
error('Cannot create cache directory, '..cacheDir:getPath())
52+
end
53+
end
54+
self.cacheDir = cacheDir
55+
self.ffmpegPath = options.ffmpeg
56+
if options.ffprobe then
57+
self.ffprobePath = options.ffprobe
58+
else
59+
local ffDir = File:new(self.ffmpegPath):getParentFile()
60+
if ffDir then
61+
self.ffprobePath = File:new(ffDir, getExecutableName('ffprobe')):getPath()
62+
else
63+
self.ffprobePath = getExecutableName('ffprobe')
64+
end
65+
end
66+
self.sources = {}
67+
self.execWorker = execWorker
68+
end
69+
70+
function ffmpeg:check()
71+
local ffmpegFile = File:new(self.ffmpegPath)
72+
if not ffmpegFile:exists() then
73+
error('ffmpeg not found, '..ffmpegFile:getPath())
74+
end
75+
local ffprobeFile = File:new(self.ffprobePath)
76+
if not ffprobeFile:exists() then
77+
error('ffprobe not found, '..ffprobeFile:getPath())
78+
end
79+
end
80+
81+
function ffmpeg:getCacheDir()
82+
return self.cacheDir
83+
end
84+
85+
function ffmpeg:prepare()
86+
end
87+
88+
local function formatTime(v, showMs)
89+
local h, m, s, ms
90+
ms = math.floor(v * 1000) % 1000
91+
v = math.floor(v)
92+
h = v // 3600
93+
m = (v % 3600) // 60
94+
s = v % 60
95+
if showMs or (ms > 0) then
96+
return string.format('%02d:%02d:%02d.%03d', h, m, s, ms)
97+
end
98+
return string.format('%02d:%02d:%02d', h, m, s)
99+
end
100+
101+
local function parseTime(v)
102+
local h, m, s, ms = string.match(v, '^(%d+):(%d+):(%d+).?(%d*)$')
103+
h = tonumber(h)
104+
m = tonumber(m)
105+
s = tonumber(s)
106+
ms = tonumber('0.'..ms)
107+
if ms == 0 then
108+
ms = 0
109+
end
110+
return (h * 60 + m) * 60 + s + ms
111+
end
112+
113+
function ffmpeg:computeArguments(destFilename, destOptions, srcFilename, srcOptions, globalOptions)
114+
local args = {self.ffmpegPath, '-hide_banner'}
115+
if globalOptions then
116+
TableList.concat(args, globalOptions)
117+
end
118+
if srcOptions then
119+
TableList.concat(args, srcOptions)
120+
end
121+
if srcFilename then
122+
TableList.concat(args, '-i', srcFilename)
123+
end
124+
if destOptions then
125+
TableList.concat(args, destOptions)
126+
end
127+
if destFilename then
128+
TableList.concat(args, '-y', destFilename)
129+
end
130+
return args
131+
end
132+
133+
function ffmpeg:createCommand(part, filename, options, seekDelay)
134+
--[[
135+
'-ss position (input/output)'
136+
When used as an input option (before -i), seeks in this input file to position.
137+
When used as an output option (before an output filename), decodes but discards input until the timestamps reach position.
138+
This is slower, but more accurate. position may be either in seconds or in hh:mm:ss[.xxx] form.
139+
140+
Seek delay in milli seconds
141+
When positive, it is used for combined seeking.
142+
Default value -1 means input seeking and as of FFmpeg 2.1 is a now also "frame-accurate".
143+
Value -2 means output seeking which is very slow.
144+
See https://trac.ffmpeg.org/wiki/Seeking
145+
146+
'-to position (output)'
147+
Stop writing the output at position. position may be a number in seconds, or in hh:mm:ss[.xxx] form.
148+
-to and -t are mutually exclusive and -t has priority.
149+
'-vframes number (output)'
150+
Set the number of video frames to record. This is an alias for -frames:v.
151+
]]
152+
local srcOptions = {}
153+
local destOptions = {}
154+
if part.from ~= nil then
155+
local delay = seekDelay or 0
156+
if (delay >= 0) and (delay < part.from) then
157+
TableList.concat(srcOptions, '-ss', formatTime(part.from - delay))
158+
TableList.concat(destOptions, '-ss', math.floor(delay))
159+
elseif delay == -1 then
160+
TableList.concat(srcOptions, '-ss', formatTime(part.from))
161+
else
162+
TableList.concat(destOptions, '-ss', formatTime(part.from))
163+
end
164+
end
165+
if part.to ~= nil then
166+
if part.from ~= nil then
167+
TableList.concat(destOptions, '-t', formatTime(part.to - part.from))
168+
--TableList.concat(destOptions, '-to', formatTime(part.to))
169+
else
170+
TableList.concat(destOptions, '-t', formatTime(part.to))
171+
end
172+
end
173+
TableList.concat(destOptions, options)
174+
local sourceFile = self.sources[part.sourceId]
175+
return self:computeArguments(filename, destOptions, sourceFile:getPath(), srcOptions)
176+
end
177+
178+
function ffmpeg:createTempFile(filename)
179+
return File:new(self.cacheDir, filename)
180+
end
181+
182+
function ffmpeg:createCommands(filename, parts, destOptions, seekDelayMs)
183+
local commands = {}
184+
if #parts == 1 then
185+
table.insert(commands, {
186+
line = self:createCommand(parts[1], filename, destOptions, seekDelayMs),
187+
name = 'Processing "'..filename..'"',
188+
showStandardError = true
189+
})
190+
elseif #parts > 1 then
191+
local concatScript = StringBuffer:new()
192+
concatScript:append('# fcut')
193+
for i, part in ipairs(parts) do
194+
local partName = 'part_'..tostring(i)..'.tmp'
195+
local outFilename = self:createTempFile(partName):getPath()
196+
table.insert(commands, {
197+
line = self:createCommand(part, outFilename, destOptions, seekDelayMs),
198+
name = 'Processing part '..tostring(i)..'/'..tostring(#parts),
199+
showStandardError = true
200+
})
201+
local concatPartname = string.gsub(outFilename, '[\\+]', '/')
202+
--local concatPartname = partName -- to be safe
203+
concatScript:append('\nfile ', concatPartname)
204+
end
205+
local concatFile = self:createTempFile('concat.txt');
206+
concatFile:write(concatScript:toString());
207+
table.insert(commands, {
208+
line = self:computeArguments(filename, {'-c', 'copy'}, concatFile:getPath(), {'-f', 'concat', '-safe', '0'}),
209+
name = 'Concat "'..filename..'"',
210+
showStandardError = true
211+
})
212+
end
213+
return commands
214+
end
215+
216+
function ffmpeg:openSource(filename)
217+
return Promise:new(function(resolve, reject)
218+
local file = File:new(filename)
219+
if not file:isFile() then
220+
reject('File not found')
221+
return
222+
end
223+
local id = computeFileId(file)
224+
self.sources[id] = file
225+
local sourceCache = File:new(self.cacheDir, id)
226+
if not sourceCache:isDirectory() then
227+
if not sourceCache:mkdir() then
228+
reject('Unable to create cache directory')
229+
return
230+
end
231+
end
232+
resolve(id)
233+
end)
234+
end
235+
236+
function ffmpeg:extractInfo(id, file)
237+
local sourceFile = self.sources[id]
238+
if not sourceFile then
239+
Promise.reject('Unknown source id "'..id..'"');
240+
end
241+
local args = {self.ffprobePath, '-hide_banner', '-v', '0', '-show_format', '-show_streams', '-of', 'json', sourceFile:getPath(), '>', file:getPath()}
242+
local line = runtime.formatCommandLine(args)
243+
--logger:fine('extractInfo execute ->'..line..'<-')
244+
return self.execWorker:execute(line)
245+
end
246+
247+
function ffmpeg:extractPreviewAt(id, sec, file, width, height)
248+
local sourceFile = self.sources[id]
249+
if not sourceFile then
250+
Promise.reject('Unknown source id "'..id..'"');
251+
end
252+
local time = LocalDateTime:new():plusSeconds(sec or 0):toTimeString()
253+
local args = {self.ffmpegPath, '-hide_banner', '-v', '0', '-ss', time, '-i', sourceFile:getPath(), '-f', 'singlejpeg', '-vcodec', 'mjpeg', '-vframes', '1', '-an'}
254+
if width and height then
255+
TableList.concat(args, '-s', tostring(width)..'x'..tostring(height))
256+
end
257+
TableList.concat(args, '-y', file:getPath())
258+
local line = runtime.formatCommandLine(args)
259+
--logger:fine('extractPreviewAt execute ->'..line..'<-')
260+
return self.execWorker:execute(line, 'preview')
261+
end
262+
263+
end)

0 commit comments

Comments
 (0)