agi.ffmpeg.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. package agi
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "github.com/robertkrimen/otto"
  9. uuid "github.com/satori/go.uuid"
  10. "imuslab.com/arozos/mod/agi/static"
  11. "imuslab.com/arozos/mod/agi/static/ffmpegutil"
  12. "imuslab.com/arozos/mod/info/logger"
  13. "imuslab.com/arozos/mod/utils"
  14. )
  15. /*
  16. AJGI FFmpeg adaptor Library
  17. This is a library for allow the use of ffmpeg via the arozos virtualized layer
  18. without the danger of directly accessing the bash / shell interface.
  19. Author: tobychui
  20. */
  21. func (g *Gateway) FFmpegLibRegister() {
  22. _, err := exec.LookPath("ffmpeg")
  23. if err != nil {
  24. logger.PrintAndLog("Agi", "ffmpeg not found in PATH", nil)
  25. return
  26. }
  27. err = g.RegisterLib("ffmpeg", g.injectFFmpegFunctions)
  28. if err != nil {
  29. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  30. return
  31. }
  32. }
  33. func (g *Gateway) injectFFmpegFunctions(payload *static.AgiLibInjectionPayload) {
  34. vm := payload.VM
  35. u := payload.User
  36. scriptFsh := payload.ScriptFsh
  37. //scriptPath := payload.ScriptPath
  38. //w := payload.Writer
  39. //r := payload.Request
  40. vm.Set("_ffmpeg_conv", func(call otto.FunctionCall) otto.Value {
  41. //Get the input and output filepath
  42. vinput, err := call.Argument(0).ToString()
  43. if err != nil {
  44. g.RaiseError(err)
  45. return otto.FalseValue()
  46. }
  47. voutput, err := call.Argument(1).ToString()
  48. if err != nil {
  49. g.RaiseError(err)
  50. return otto.FalseValue()
  51. }
  52. if voutput == "" {
  53. //Output filename not provided. Not sure what format to convert
  54. g.RaiseError(errors.New("output filename not provided"))
  55. return otto.FalseValue()
  56. }
  57. compression, err := call.Argument(2).ToInteger()
  58. if err != nil {
  59. //Do not use compression
  60. compression = 0
  61. }
  62. //Rewrite the vpath if it is relative
  63. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  64. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  65. //Translate the virtual path to realpath for the input file
  66. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  67. if err != nil {
  68. g.RaiseError(err)
  69. return otto.FalseValue()
  70. }
  71. //Translate the virtual path to realpath for the output file
  72. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  73. if err != nil {
  74. g.RaiseError(err)
  75. return otto.FalseValue()
  76. }
  77. //Buffer the file to tmp
  78. //Note that even for local disk, it still need to be buffered to make sure
  79. //permission is in-scope as well as to avoid locking a file by child-process
  80. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  81. if err != nil {
  82. g.RaiseError(err)
  83. return otto.FalseValue()
  84. }
  85. //fmt.Println(rinput, routput, bufferedFilepath)
  86. //Convert it to target format using ffmpeg
  87. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  88. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  89. err = ffmpegutil.FFmpeg_conv(bufferedFilepath, outputBufferPath, int(compression))
  90. if err != nil {
  91. //FFmpeg conversion failed
  92. g.RaiseError(err)
  93. //Delete the buffered file
  94. os.Remove(bufferedFilepath)
  95. return otto.FalseValue()
  96. }
  97. if !utils.FileExists(outputBufferPath) {
  98. //Fallback check, to see if the output file actually exists
  99. g.RaiseError(errors.New("output file not found. Assume ffmpeg conversion failed"))
  100. //Delete the buffered file
  101. os.Remove(bufferedFilepath)
  102. return otto.FalseValue()
  103. }
  104. //Conversion completed
  105. //Delete the buffered file
  106. os.Remove(bufferedFilepath)
  107. //Upload the converted file to target disk
  108. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  109. if err != nil {
  110. g.RaiseError(err)
  111. //Delete the output buffer if failed
  112. os.Remove(outputBufferPath)
  113. return otto.FalseValue()
  114. }
  115. defer src.Close()
  116. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  117. if err != nil {
  118. g.RaiseError(err)
  119. //Delete the output buffer if failed
  120. os.Remove(outputBufferPath)
  121. return otto.FalseValue()
  122. }
  123. //Upload completed. Remove the remaining buffer file
  124. os.Remove(outputBufferPath)
  125. return otto.TrueValue()
  126. })
  127. // _ffmpeg_audio_conv(input, output, sampleRate, progressFile)
  128. // Converts audio (or strips audio from video).
  129. // sampleRate: target Hz, e.g. 44100; 0 keeps original.
  130. // progressFile: virtual path for the JSON progress file; omit or pass "" to disable.
  131. vm.Set("_ffmpeg_audio_conv", func(call otto.FunctionCall) otto.Value {
  132. vinput, err := call.Argument(0).ToString()
  133. if err != nil {
  134. g.RaiseError(err)
  135. return otto.FalseValue()
  136. }
  137. voutput, err := call.Argument(1).ToString()
  138. if err != nil || voutput == "" || voutput == "undefined" {
  139. g.RaiseError(errors.New("output filename not provided"))
  140. return otto.FalseValue()
  141. }
  142. sampleRate, err := call.Argument(2).ToInteger()
  143. if err != nil || call.Argument(2).IsUndefined() {
  144. sampleRate = 0
  145. }
  146. vprogressFile := ""
  147. if !call.Argument(3).IsUndefined() {
  148. vprogressFile, _ = call.Argument(3).ToString()
  149. }
  150. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  151. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  152. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  153. if err != nil {
  154. g.RaiseError(err)
  155. return otto.FalseValue()
  156. }
  157. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  158. if err != nil {
  159. g.RaiseError(err)
  160. return otto.FalseValue()
  161. }
  162. rprogressFile := ""
  163. if vprogressFile != "" && vprogressFile != "undefined" {
  164. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  165. if _, rp, e := static.VirtualPathToRealPath(vprogressFile, u); e == nil {
  166. rprogressFile = rp
  167. }
  168. }
  169. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  170. if err != nil {
  171. g.RaiseError(err)
  172. return otto.FalseValue()
  173. }
  174. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  175. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  176. err = ffmpegutil.FFmpeg_audio_conv(bufferedFilepath, outputBufferPath, int(sampleRate), rprogressFile)
  177. os.Remove(bufferedFilepath)
  178. if err != nil {
  179. g.RaiseError(err)
  180. //Remove the partial output left behind by a failed or cancelled conversion
  181. os.Remove(outputBufferPath)
  182. return otto.FalseValue()
  183. }
  184. if !utils.FileExists(outputBufferPath) {
  185. g.RaiseError(errors.New("output file not found after audio conversion"))
  186. return otto.FalseValue()
  187. }
  188. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  189. if err != nil {
  190. g.RaiseError(err)
  191. os.Remove(outputBufferPath)
  192. return otto.FalseValue()
  193. }
  194. defer src.Close()
  195. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  196. if err != nil {
  197. g.RaiseError(err)
  198. os.Remove(outputBufferPath)
  199. return otto.FalseValue()
  200. }
  201. os.Remove(outputBufferPath)
  202. return otto.TrueValue()
  203. })
  204. // _ffmpeg_image_conv(input, output, scaleFactor, compressionRate)
  205. // Converts an image file with optional uniform scaling and lossy compression.
  206. // scaleFactor: float multiplier for both dimensions (0.5 = half size); 0 or 1.0 = no change.
  207. // compressionRate: 0-100; only applied to lossy formats (JPEG, WebP); ignored for PNG/BMP/GIF.
  208. vm.Set("_ffmpeg_image_conv", func(call otto.FunctionCall) otto.Value {
  209. vinput, err := call.Argument(0).ToString()
  210. if err != nil {
  211. g.RaiseError(err)
  212. return otto.FalseValue()
  213. }
  214. voutput, err := call.Argument(1).ToString()
  215. if err != nil || voutput == "" || voutput == "undefined" {
  216. g.RaiseError(errors.New("output filename not provided"))
  217. return otto.FalseValue()
  218. }
  219. scaleFactor, err := call.Argument(2).ToFloat()
  220. if err != nil || call.Argument(2).IsUndefined() {
  221. scaleFactor = 0
  222. }
  223. compressionRate, err := call.Argument(3).ToInteger()
  224. if err != nil || call.Argument(3).IsUndefined() {
  225. compressionRate = 0
  226. }
  227. vprogressFile := ""
  228. if !call.Argument(4).IsUndefined() {
  229. vprogressFile, _ = call.Argument(4).ToString()
  230. }
  231. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  232. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  233. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  234. if err != nil {
  235. g.RaiseError(err)
  236. return otto.FalseValue()
  237. }
  238. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  239. if err != nil {
  240. g.RaiseError(err)
  241. return otto.FalseValue()
  242. }
  243. rprogressFile := ""
  244. if vprogressFile != "" && vprogressFile != "undefined" {
  245. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  246. if _, rp, e := static.VirtualPathToRealPath(vprogressFile, u); e == nil {
  247. rprogressFile = rp
  248. }
  249. }
  250. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  251. if err != nil {
  252. g.RaiseError(err)
  253. return otto.FalseValue()
  254. }
  255. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  256. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  257. err = ffmpegutil.FFmpeg_image_conv(bufferedFilepath, outputBufferPath, scaleFactor, int(compressionRate), rprogressFile)
  258. os.Remove(bufferedFilepath)
  259. if err != nil {
  260. g.RaiseError(err)
  261. //Remove the partial output left behind by a failed or cancelled conversion
  262. os.Remove(outputBufferPath)
  263. return otto.FalseValue()
  264. }
  265. if !utils.FileExists(outputBufferPath) {
  266. g.RaiseError(errors.New("output file not found after image conversion"))
  267. return otto.FalseValue()
  268. }
  269. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  270. if err != nil {
  271. g.RaiseError(err)
  272. os.Remove(outputBufferPath)
  273. return otto.FalseValue()
  274. }
  275. defer src.Close()
  276. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  277. if err != nil {
  278. g.RaiseError(err)
  279. os.Remove(outputBufferPath)
  280. return otto.FalseValue()
  281. }
  282. os.Remove(outputBufferPath)
  283. return otto.TrueValue()
  284. })
  285. // _ffmpeg_video_conv(input, output, resolution, compressionRate, progressFile)
  286. // Converts a video file with optional resolution scaling and CRF compression.
  287. // resolution: "144p", "240p", "360p", "480p", "576p", "720p", "1080p", "1440p", "2160p", "4k", "8k"; "" keeps original.
  288. // compressionRate: 0-100; mapped to CRF 1-51 (0 = encoder default, 100 = most compressed).
  289. // progressFile: virtual path for the JSON progress file; omit or pass "" to disable.
  290. vm.Set("_ffmpeg_video_conv", func(call otto.FunctionCall) otto.Value {
  291. vinput, err := call.Argument(0).ToString()
  292. if err != nil {
  293. g.RaiseError(err)
  294. return otto.FalseValue()
  295. }
  296. voutput, err := call.Argument(1).ToString()
  297. if err != nil || voutput == "" || voutput == "undefined" {
  298. g.RaiseError(errors.New("output filename not provided"))
  299. return otto.FalseValue()
  300. }
  301. resolution := ""
  302. if !call.Argument(2).IsUndefined() {
  303. resolution, _ = call.Argument(2).ToString()
  304. if resolution == "undefined" {
  305. resolution = ""
  306. }
  307. }
  308. compressionRate, err := call.Argument(3).ToInteger()
  309. if err != nil || call.Argument(3).IsUndefined() {
  310. compressionRate = 0
  311. }
  312. vprogressFile := ""
  313. if !call.Argument(4).IsUndefined() {
  314. vprogressFile, _ = call.Argument(4).ToString()
  315. }
  316. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  317. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  318. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  319. if err != nil {
  320. g.RaiseError(err)
  321. return otto.FalseValue()
  322. }
  323. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  324. if err != nil {
  325. g.RaiseError(err)
  326. return otto.FalseValue()
  327. }
  328. rprogressFile := ""
  329. if vprogressFile != "" && vprogressFile != "undefined" {
  330. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  331. if _, rp, e := static.VirtualPathToRealPath(vprogressFile, u); e == nil {
  332. rprogressFile = rp
  333. }
  334. }
  335. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  336. if err != nil {
  337. g.RaiseError(err)
  338. return otto.FalseValue()
  339. }
  340. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  341. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  342. err = ffmpegutil.FFmpeg_video_conv(bufferedFilepath, outputBufferPath, resolution, int(compressionRate), rprogressFile)
  343. os.Remove(bufferedFilepath)
  344. if err != nil {
  345. g.RaiseError(err)
  346. //Remove the partial output left behind by a failed or cancelled conversion
  347. os.Remove(outputBufferPath)
  348. return otto.FalseValue()
  349. }
  350. if !utils.FileExists(outputBufferPath) {
  351. g.RaiseError(errors.New("output file not found after video conversion"))
  352. return otto.FalseValue()
  353. }
  354. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  355. if err != nil {
  356. g.RaiseError(err)
  357. os.Remove(outputBufferPath)
  358. return otto.FalseValue()
  359. }
  360. defer src.Close()
  361. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  362. if err != nil {
  363. g.RaiseError(err)
  364. os.Remove(outputBufferPath)
  365. return otto.FalseValue()
  366. }
  367. os.Remove(outputBufferPath)
  368. return otto.TrueValue()
  369. })
  370. // _ffmpeg_conv_with_progress(input, output, progressFile)
  371. // Passes input directly to ffmpeg without format detection.
  372. // Suitable for cross-media conversions (e.g. mp4-->gif) or unknown format pairs.
  373. // progressFile: virtual path for the JSON progress file; omit or pass "" to disable.
  374. vm.Set("_ffmpeg_conv_with_progress", func(call otto.FunctionCall) otto.Value {
  375. vinput, err := call.Argument(0).ToString()
  376. if err != nil {
  377. g.RaiseError(err)
  378. return otto.FalseValue()
  379. }
  380. voutput, err := call.Argument(1).ToString()
  381. if err != nil || voutput == "" || voutput == "undefined" {
  382. g.RaiseError(errors.New("output filename not provided"))
  383. return otto.FalseValue()
  384. }
  385. vprogressFile := ""
  386. if !call.Argument(2).IsUndefined() {
  387. vprogressFile, _ = call.Argument(2).ToString()
  388. }
  389. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  390. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  391. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  392. if err != nil {
  393. g.RaiseError(err)
  394. return otto.FalseValue()
  395. }
  396. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  397. if err != nil {
  398. g.RaiseError(err)
  399. return otto.FalseValue()
  400. }
  401. rprogressFile := ""
  402. if vprogressFile != "" && vprogressFile != "undefined" {
  403. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  404. if _, rp, e := static.VirtualPathToRealPath(vprogressFile, u); e == nil {
  405. rprogressFile = rp
  406. }
  407. }
  408. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  409. if err != nil {
  410. g.RaiseError(err)
  411. return otto.FalseValue()
  412. }
  413. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  414. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  415. err = ffmpegutil.FFmpeg_conv_with_progress(bufferedFilepath, outputBufferPath, rprogressFile)
  416. os.Remove(bufferedFilepath)
  417. if err != nil {
  418. g.RaiseError(err)
  419. //Remove the partial output left behind by a failed or cancelled conversion
  420. os.Remove(outputBufferPath)
  421. return otto.FalseValue()
  422. }
  423. if !utils.FileExists(outputBufferPath) {
  424. g.RaiseError(errors.New("output file not found after conversion"))
  425. return otto.FalseValue()
  426. }
  427. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  428. if err != nil {
  429. g.RaiseError(err)
  430. os.Remove(outputBufferPath)
  431. return otto.FalseValue()
  432. }
  433. defer src.Close()
  434. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  435. if err != nil {
  436. g.RaiseError(err)
  437. os.Remove(outputBufferPath)
  438. return otto.FalseValue()
  439. }
  440. os.Remove(outputBufferPath)
  441. return otto.TrueValue()
  442. })
  443. // _ffmpeg_cancel(progressFile)
  444. // Stops the conversion that was started with the given progress file, no matter
  445. // which request or user script started it. Returns true if a running conversion
  446. // was found and terminated, false if it already finished or never had a progress
  447. // file. The cancelled conversion reports itself as a failed conversion.
  448. vm.Set("_ffmpeg_cancel", func(call otto.FunctionCall) otto.Value {
  449. vprogressFile, err := call.Argument(0).ToString()
  450. if err != nil || vprogressFile == "" || vprogressFile == "undefined" {
  451. g.RaiseError(errors.New("progress file not provided"))
  452. return otto.FalseValue()
  453. }
  454. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  455. _, rprogressFile, err := static.VirtualPathToRealPath(vprogressFile, u)
  456. if err != nil {
  457. g.RaiseError(err)
  458. return otto.FalseValue()
  459. }
  460. if !ffmpegutil.CancelConversion(rprogressFile) {
  461. return otto.FalseValue()
  462. }
  463. return otto.TrueValue()
  464. })
  465. vm.Run(`
  466. var ffmpeg = {};
  467. ffmpeg.convert = _ffmpeg_conv;
  468. ffmpeg.audioConvert = _ffmpeg_audio_conv;
  469. ffmpeg.imageConvert = _ffmpeg_image_conv;
  470. ffmpeg.videoConvert = _ffmpeg_video_conv;
  471. ffmpeg.convertWithProgress = _ffmpeg_conv_with_progress;
  472. ffmpeg.cancel = _ffmpeg_cancel;
  473. `)
  474. }