agi.ffmpeg.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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. return otto.FalseValue()
  181. }
  182. if !utils.FileExists(outputBufferPath) {
  183. g.RaiseError(errors.New("output file not found after audio conversion"))
  184. return otto.FalseValue()
  185. }
  186. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  187. if err != nil {
  188. g.RaiseError(err)
  189. os.Remove(outputBufferPath)
  190. return otto.FalseValue()
  191. }
  192. defer src.Close()
  193. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  194. if err != nil {
  195. g.RaiseError(err)
  196. os.Remove(outputBufferPath)
  197. return otto.FalseValue()
  198. }
  199. os.Remove(outputBufferPath)
  200. return otto.TrueValue()
  201. })
  202. // _ffmpeg_image_conv(input, output, scaleFactor, compressionRate)
  203. // Converts an image file with optional uniform scaling and lossy compression.
  204. // scaleFactor: float multiplier for both dimensions (0.5 = half size); 0 or 1.0 = no change.
  205. // compressionRate: 0-100; only applied to lossy formats (JPEG, WebP); ignored for PNG/BMP/GIF.
  206. vm.Set("_ffmpeg_image_conv", func(call otto.FunctionCall) otto.Value {
  207. vinput, err := call.Argument(0).ToString()
  208. if err != nil {
  209. g.RaiseError(err)
  210. return otto.FalseValue()
  211. }
  212. voutput, err := call.Argument(1).ToString()
  213. if err != nil || voutput == "" || voutput == "undefined" {
  214. g.RaiseError(errors.New("output filename not provided"))
  215. return otto.FalseValue()
  216. }
  217. scaleFactor, err := call.Argument(2).ToFloat()
  218. if err != nil || call.Argument(2).IsUndefined() {
  219. scaleFactor = 0
  220. }
  221. compressionRate, err := call.Argument(3).ToInteger()
  222. if err != nil || call.Argument(3).IsUndefined() {
  223. compressionRate = 0
  224. }
  225. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  226. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  227. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  228. if err != nil {
  229. g.RaiseError(err)
  230. return otto.FalseValue()
  231. }
  232. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  233. if err != nil {
  234. g.RaiseError(err)
  235. return otto.FalseValue()
  236. }
  237. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  238. if err != nil {
  239. g.RaiseError(err)
  240. return otto.FalseValue()
  241. }
  242. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  243. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  244. err = ffmpegutil.FFmpeg_image_conv(bufferedFilepath, outputBufferPath, scaleFactor, int(compressionRate))
  245. os.Remove(bufferedFilepath)
  246. if err != nil {
  247. g.RaiseError(err)
  248. return otto.FalseValue()
  249. }
  250. if !utils.FileExists(outputBufferPath) {
  251. g.RaiseError(errors.New("output file not found after image conversion"))
  252. return otto.FalseValue()
  253. }
  254. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  255. if err != nil {
  256. g.RaiseError(err)
  257. os.Remove(outputBufferPath)
  258. return otto.FalseValue()
  259. }
  260. defer src.Close()
  261. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  262. if err != nil {
  263. g.RaiseError(err)
  264. os.Remove(outputBufferPath)
  265. return otto.FalseValue()
  266. }
  267. os.Remove(outputBufferPath)
  268. return otto.TrueValue()
  269. })
  270. // _ffmpeg_video_conv(input, output, resolution, compressionRate, progressFile)
  271. // Converts a video file with optional resolution scaling and CRF compression.
  272. // resolution: "144p", "240p", "360p", "480p", "576p", "720p", "1080p", "1440p", "2160p", "4k", "8k"; "" keeps original.
  273. // compressionRate: 0-100; mapped to CRF 1-51 (0 = encoder default, 100 = most compressed).
  274. // progressFile: virtual path for the JSON progress file; omit or pass "" to disable.
  275. vm.Set("_ffmpeg_video_conv", func(call otto.FunctionCall) otto.Value {
  276. vinput, err := call.Argument(0).ToString()
  277. if err != nil {
  278. g.RaiseError(err)
  279. return otto.FalseValue()
  280. }
  281. voutput, err := call.Argument(1).ToString()
  282. if err != nil || voutput == "" || voutput == "undefined" {
  283. g.RaiseError(errors.New("output filename not provided"))
  284. return otto.FalseValue()
  285. }
  286. resolution := ""
  287. if !call.Argument(2).IsUndefined() {
  288. resolution, _ = call.Argument(2).ToString()
  289. if resolution == "undefined" {
  290. resolution = ""
  291. }
  292. }
  293. compressionRate, err := call.Argument(3).ToInteger()
  294. if err != nil || call.Argument(3).IsUndefined() {
  295. compressionRate = 0
  296. }
  297. vprogressFile := ""
  298. if !call.Argument(4).IsUndefined() {
  299. vprogressFile, _ = call.Argument(4).ToString()
  300. }
  301. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  302. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  303. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  304. if err != nil {
  305. g.RaiseError(err)
  306. return otto.FalseValue()
  307. }
  308. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  309. if err != nil {
  310. g.RaiseError(err)
  311. return otto.FalseValue()
  312. }
  313. rprogressFile := ""
  314. if vprogressFile != "" && vprogressFile != "undefined" {
  315. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  316. if _, rp, e := static.VirtualPathToRealPath(vprogressFile, u); e == nil {
  317. rprogressFile = rp
  318. }
  319. }
  320. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  321. if err != nil {
  322. g.RaiseError(err)
  323. return otto.FalseValue()
  324. }
  325. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  326. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  327. err = ffmpegutil.FFmpeg_video_conv(bufferedFilepath, outputBufferPath, resolution, int(compressionRate), rprogressFile)
  328. os.Remove(bufferedFilepath)
  329. if err != nil {
  330. g.RaiseError(err)
  331. return otto.FalseValue()
  332. }
  333. if !utils.FileExists(outputBufferPath) {
  334. g.RaiseError(errors.New("output file not found after video conversion"))
  335. return otto.FalseValue()
  336. }
  337. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  338. if err != nil {
  339. g.RaiseError(err)
  340. os.Remove(outputBufferPath)
  341. return otto.FalseValue()
  342. }
  343. defer src.Close()
  344. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  345. if err != nil {
  346. g.RaiseError(err)
  347. os.Remove(outputBufferPath)
  348. return otto.FalseValue()
  349. }
  350. os.Remove(outputBufferPath)
  351. return otto.TrueValue()
  352. })
  353. // _ffmpeg_conv_with_progress(input, output, progressFile)
  354. // Passes input directly to ffmpeg without format detection.
  355. // Suitable for cross-media conversions (e.g. mp4→gif) or unknown format pairs.
  356. // progressFile: virtual path for the JSON progress file; omit or pass "" to disable.
  357. vm.Set("_ffmpeg_conv_with_progress", func(call otto.FunctionCall) otto.Value {
  358. vinput, err := call.Argument(0).ToString()
  359. if err != nil {
  360. g.RaiseError(err)
  361. return otto.FalseValue()
  362. }
  363. voutput, err := call.Argument(1).ToString()
  364. if err != nil || voutput == "" || voutput == "undefined" {
  365. g.RaiseError(errors.New("output filename not provided"))
  366. return otto.FalseValue()
  367. }
  368. vprogressFile := ""
  369. if !call.Argument(2).IsUndefined() {
  370. vprogressFile, _ = call.Argument(2).ToString()
  371. }
  372. vinput = static.RelativeVpathRewrite(scriptFsh, vinput, vm, u)
  373. voutput = static.RelativeVpathRewrite(scriptFsh, voutput, vm, u)
  374. fsh, rinput, err := static.VirtualPathToRealPath(vinput, u)
  375. if err != nil {
  376. g.RaiseError(err)
  377. return otto.FalseValue()
  378. }
  379. fsh, routput, err := static.VirtualPathToRealPath(voutput, u)
  380. if err != nil {
  381. g.RaiseError(err)
  382. return otto.FalseValue()
  383. }
  384. rprogressFile := ""
  385. if vprogressFile != "" && vprogressFile != "undefined" {
  386. vprogressFile = static.RelativeVpathRewrite(scriptFsh, vprogressFile, vm, u)
  387. if _, rp, e := static.VirtualPathToRealPath(vprogressFile, u); e == nil {
  388. rprogressFile = rp
  389. }
  390. }
  391. bufferedFilepath, err := fsh.BufferRemoteToLocal(rinput)
  392. if err != nil {
  393. g.RaiseError(err)
  394. return otto.FalseValue()
  395. }
  396. outputTmpFilename := uuid.NewV4().String() + filepath.Ext(routput)
  397. outputBufferPath := filepath.Join(filepath.Dir(bufferedFilepath), outputTmpFilename)
  398. err = ffmpegutil.FFmpeg_conv_with_progress(bufferedFilepath, outputBufferPath, rprogressFile)
  399. os.Remove(bufferedFilepath)
  400. if err != nil {
  401. g.RaiseError(err)
  402. return otto.FalseValue()
  403. }
  404. if !utils.FileExists(outputBufferPath) {
  405. g.RaiseError(errors.New("output file not found after conversion"))
  406. return otto.FalseValue()
  407. }
  408. src, err := os.OpenFile(outputBufferPath, os.O_RDONLY, 0755)
  409. if err != nil {
  410. g.RaiseError(err)
  411. os.Remove(outputBufferPath)
  412. return otto.FalseValue()
  413. }
  414. defer src.Close()
  415. err = fsh.FileSystemAbstraction.WriteStream(routput, src, 0775)
  416. if err != nil {
  417. g.RaiseError(err)
  418. os.Remove(outputBufferPath)
  419. return otto.FalseValue()
  420. }
  421. os.Remove(outputBufferPath)
  422. return otto.TrueValue()
  423. })
  424. vm.Run(`
  425. var ffmpeg = {};
  426. ffmpeg.convert = _ffmpeg_conv;
  427. ffmpeg.audioConvert = _ffmpeg_audio_conv;
  428. ffmpeg.imageConvert = _ffmpeg_image_conv;
  429. ffmpeg.videoConvert = _ffmpeg_video_conv;
  430. ffmpeg.convertWithProgress = _ffmpeg_conv_with_progress;
  431. `)
  432. }