mpegize.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. #!/usr/bin/python3
  2. # Copyright 2022, 2024 Pavel Machek, GPLv2+
  3. import os, sys, time, copy, subprocess
  4. import gi
  5. gi.require_version('Gst', '1.0')
  6. from gi.repository import Gst, GLib
  7. import os
  8. import time
  9. # https://stackoverflow.com/questions/11779490/how-to-add-a-new-audio-not-mixing-into-a-video-using-ffmpeg
  10. # https://ottverse.com/create-video-from-images-using-ffmpeg/
  11. # https://github.com/kkroening/ffmpeg-python/issues/95
  12. # sudo apt install ffmpeg
  13. # Usage: mpegize convert
  14. # head -c 1000 < /dev/zero > /tmp/delme.sm/1.foo.sa
  15. def gst_convert(mega_dir, out_file, use_jpeg):
  16. def sa_read(name, t):
  17. with open(name, "rb") as file:
  18. rgb_data = file.read(10*1024*1024)
  19. caps_string = "audio/x-raw,format=U16LE,channels=2,rate=48000,layout=interleaved,channel-mask=3"
  20. caps = Gst.Caps.from_string(caps_string)
  21. buffer = Gst.Buffer.new_wrapped(rgb_data)
  22. if False:
  23. time.sleep(1/30.)
  24. # nanoseconds
  25. buffer.pts = time.time() * 1000*1000*1000
  26. buffer.dts = time.time() * 1000*1000*1000
  27. elif True:
  28. buffer.pts = t
  29. buffer.dts = t
  30. buffer.duration = (1000*1000*1000)/10.
  31. return buffer, caps
  32. def sa_src(appsrc):
  33. def on_need_data(appsrc, data, length):
  34. name = audio.get_path()
  35. if name == None or name[-22:] != ".48000-s16le-stereo.sa":
  36. appsrc.emit("end-of-stream")
  37. print("End of audio stream")
  38. return
  39. t = audio.get_time()
  40. #print("Audio: ", name, " need ", data, t)
  41. audio.pop()
  42. buffer, caps = sa_read(name, t)
  43. appsrc.set_property("caps", caps)
  44. appsrc.emit("push-buffer", buffer)
  45. appsrc.set_property("format", Gst.Format.TIME)
  46. appsrc.set_property("is-live", False)
  47. appsrc.set_property("block", True)
  48. name = audio.get_path()
  49. buffer, caps = sa_read(name, 0)
  50. appsrc.set_property("caps", caps)
  51. #appsrc.emit("push-buffer", buffer)
  52. s = appsrc.connect("need-data", on_need_data, "")
  53. print("Connect", s)
  54. class grwBase:
  55. def init(m, dir):
  56. m.dir = dir
  57. m.list = os.listdir(dir)
  58. m.list.sort()
  59. m.slen = len(m.suffix)
  60. m.start_time = 0
  61. print("Movie", len(m.list))
  62. def get_path(m):
  63. s = m.get_name()
  64. if s: return m.dir + s
  65. return s
  66. def get_name(m):
  67. #print("Get path -- ")
  68. while True:
  69. if (len(m.list)) == 0:
  70. return None
  71. #print("Get path: ", m.list[0], m.suffix)
  72. if m.list[0][-m.slen:] != m.suffix:
  73. m.pop()
  74. continue
  75. return m.list[0]
  76. def get_time(m):
  77. s = m.get_name()
  78. s = s[:-m.slen]
  79. return int(s) * 1000 - m.start_time
  80. def pop(m):
  81. m.list = m.list[1:]
  82. class grwVideo(grwBase):
  83. suffix = ".grw"
  84. def __init__(m, dir):
  85. m.init(dir)
  86. class grwJPEG(grwBase):
  87. suffix = ".jpeg.sv"
  88. def __init__(m, dir):
  89. m.init(dir + "sm/")
  90. class grwAudio(grwVideo):
  91. suffix = ".48000-s16le-stereo.sa"
  92. def __init__(m, dir):
  93. m.init(dir + "sm/")
  94. def grw_read(name, t):
  95. with open(name, "rb") as file:
  96. rgb_data = file.read(10*1024*1024)
  97. i = len(rgb_data)
  98. i -= 1
  99. while rgb_data[i] != 0:
  100. i -= 1
  101. footer = rgb_data[i+1:]
  102. sp = str(footer, 'ascii').split('\n')
  103. # Create caps for the file
  104. caps_string = sp[0][6:]
  105. caps = Gst.Caps.from_string(caps_string)
  106. if sp[0][:6] != "Caps: ":
  107. print("Bad footer")
  108. if sp[1][:6] != "Size: ":
  109. print("Bad footer")
  110. if sp[-1] != "GRW":
  111. print("Missing GRW footer")
  112. buffer = Gst.Buffer.new_wrapped(rgb_data)
  113. # This does not work for interactive use.
  114. if False:
  115. time.sleep(1/30.)
  116. # nanoseconds
  117. buffer.pts = time.time() * 1000*1000*1000
  118. buffer.dts = time.time() * 1000*1000*1000
  119. elif True:
  120. buffer.pts = t
  121. buffer.dts = t
  122. buffer.duration = (1000*1000*1000)/30.
  123. return buffer, caps
  124. def grwsrc(appsrc):
  125. def on_need_data(appsrc, data, length):
  126. name = movie.get_path()
  127. if name == None or name[-4:] != ".grw":
  128. appsrc.emit("end-of-stream")
  129. print("End of video stream")
  130. return
  131. t = movie.get_time()
  132. #print("Video: ", name, t)
  133. movie.pop()
  134. buffer, caps = grw_read(name, t)
  135. appsrc.set_property("caps", caps)
  136. appsrc.emit("push-buffer", buffer)
  137. appsrc.set_property("format", Gst.Format.TIME)
  138. appsrc.set_property("is-live", False)
  139. appsrc.set_property("block", True)
  140. name = movie.get_path()
  141. buffer, caps = grw_read(name, 0)
  142. appsrc.set_property("caps", caps)
  143. #appsrc.emit("push-buffer", buffer)
  144. s = appsrc.connect("need-data", on_need_data, "")
  145. print("Connect", s)
  146. def jpeg_read(name, t):
  147. with open(name, "rb") as file:
  148. rgb_data = file.read(10*1024*1024)
  149. i = len(rgb_data)
  150. buffer = Gst.Buffer.new_wrapped(rgb_data)
  151. caps_string = "image/jpeg"
  152. caps = Gst.Caps.from_string(caps_string)
  153. # This does not work for interactive use.
  154. if False:
  155. time.sleep(1/30.)
  156. # nanoseconds
  157. buffer.pts = time.time() * 1000*1000*1000
  158. buffer.dts = time.time() * 1000*1000*1000
  159. elif True:
  160. buffer.pts = t
  161. buffer.dts = t
  162. buffer.duration = (1000*1000*1000)/30.
  163. return buffer, caps
  164. def jpeg_src(appsrc):
  165. def on_need_data(appsrc, data, length):
  166. name = movie.get_path()
  167. if name == None or name[-8:] != ".jpeg.sv":
  168. appsrc.emit("end-of-stream")
  169. print("End of video stream")
  170. return
  171. t = movie.get_time()
  172. #print("Video: ", name, t)
  173. movie.pop()
  174. buffer, caps = jpeg_read(name, t)
  175. appsrc.set_property("caps", caps)
  176. appsrc.emit("push-buffer", buffer)
  177. appsrc.set_property("format", Gst.Format.TIME)
  178. appsrc.set_property("is-live", False)
  179. appsrc.set_property("block", True)
  180. name = movie.get_path()
  181. buffer, caps = jpeg_read(name, 0)
  182. appsrc.set_property("caps", caps)
  183. #appsrc.emit("push-buffer", buffer)
  184. s = appsrc.connect("need-data", on_need_data, "")
  185. print("Connect", s)
  186. def v_src(appsrc):
  187. if not use_jpeg:
  188. grwsrc(appsrc)
  189. else:
  190. jpeg_src(appsrc)
  191. count = 0
  192. path = mega_dir
  193. if use_jpeg:
  194. movie = grwJPEG(path)
  195. else:
  196. movie = grwVideo(path)
  197. audio = grwAudio(path)
  198. t1 = movie.get_time()
  199. t2 = audio.get_time()
  200. tm = min(t1,t2)
  201. print("Time base is", tm)
  202. movie.start_time = tm
  203. audio.start_time = tm
  204. def pipeline_video():
  205. if True:
  206. s = "appsrc name=source"
  207. if use_jpeg:
  208. s += " ! jpegdec "
  209. else:
  210. s = "videotestsrc"
  211. s += " ! video/x-raw,width=(int)640,height=(int)480,format=(string)RGB "
  212. if False:
  213. s += " ! videoconvert ! jpegenc"
  214. s += " ! appsink name=sink"
  215. elif True:
  216. s += " ! videoconvert ! autovideosink"
  217. else:
  218. s += " ! videoconvert ! x264enc bitrate=3072 speed-preset=ultrafast ! matroskamux ! filesink location=" + out_file
  219. pipeline = Gst.parse_launch(s)
  220. p = pipeline.get_by_name("source")
  221. if p:
  222. if False:
  223. mysrc(p)
  224. else:
  225. v_src(p)
  226. p = pipeline.get_by_name("sink")
  227. if p:
  228. mysink(p)
  229. return pipeline
  230. def pipeline_audio():
  231. # audiotestsrc ! audioconvert ! audioresample ! autoaudiosink
  232. if True:
  233. s = "appsrc name=source"
  234. else:
  235. s = "audiotestsrc"
  236. if True:
  237. s += " ! audiobuffersplit ! audioconvert ! audioresample ! autoaudiosink"
  238. else:
  239. s += " ! ! ! "
  240. pipeline = Gst.parse_launch(s)
  241. p = pipeline.get_by_name("source")
  242. if p:
  243. sa_src(p)
  244. p = pipeline.get_by_name("sink")
  245. if p:
  246. mysink(p)
  247. return pipeline
  248. def pipeline_both():
  249. if True:
  250. s = "appsrc name=asrc"
  251. else:
  252. s = "audiotestsrc"
  253. # Audiobuffersplit creates problems with A/V synchronization, avoid.
  254. #s += "! audiobuffersplit"
  255. s += " ! audioconvert ! vorbisenc ! mux. "
  256. if True:
  257. s += "appsrc name=vsrc"
  258. if use_jpeg:
  259. s += " ! jpegdec "
  260. else:
  261. s += "videotestsrc"
  262. s += " ! video/x-raw,width=(int)640,height=(int)480,format=(string)RGB "
  263. s += " ! videoconvert ! x264enc bitrate=3072 speed-preset=ultrafast ! matroskamux name=mux"
  264. if False:
  265. s += " ! decodebin ! playsink"
  266. else:
  267. s += " ! filesink location="+out_file
  268. pipeline = Gst.parse_launch(s)
  269. p = pipeline.get_by_name("asrc")
  270. if p:
  271. sa_src(p)
  272. p = pipeline.get_by_name("vsrc")
  273. if p:
  274. v_src(p)
  275. return pipeline
  276. Gst.init(None)
  277. Gst.debug_set_default_threshold(Gst.DebugLevel.WARNING)
  278. if False:
  279. Gst.debug_set_default_threshold(Gst.DebugLevel.INFO)
  280. if False:
  281. pipeline = pipeline_video()
  282. elif False:
  283. pipeline = pipeline_audio()
  284. else:
  285. pipeline = pipeline_both()
  286. # Function to handle end of stream
  287. def on_eos(bus, message):
  288. print("End of stream")
  289. pipeline.set_state(Gst.State.NULL)
  290. loop.quit()
  291. # Set up bus to handle messages
  292. bus = pipeline.get_bus()
  293. bus.add_signal_watch()
  294. bus.connect("message::eos", on_eos)
  295. # Set the pipeline to the playing state
  296. pipeline.set_state(Gst.State.PLAYING)
  297. # Run the main loop to handle GStreamer events
  298. loop = GLib.MainLoop()
  299. try:
  300. loop.run()
  301. except KeyboardInterrupt:
  302. pipeline.set_state(Gst.State.NULL)
  303. loop.quit()
  304. class Mpegize:
  305. base = '/tmp/delme.'
  306. fps = 30.5
  307. def prepare(m):
  308. m.source = m.base+'sm'
  309. m.work = m.base+'smt'
  310. m.output = m.base+'smo'
  311. def prepare_work(m):
  312. m.prepare()
  313. if not os.path.exists(m.output):
  314. os.mkdir(m.output)
  315. if not os.path.exists(m.work):
  316. os.mkdir(m.work)
  317. os.chdir(m.work)
  318. os.system("rm *.jpeg output.*")
  319. def prepare_source(m):
  320. m.prepare()
  321. m.out_index = 0
  322. l = os.listdir(m.source)
  323. print("Have", m.display_frames(len(l)), "frames")
  324. l.sort()
  325. m.frames = l
  326. m.unused_frames = copy.deepcopy(l)
  327. def parse_frame(m, n):
  328. if n[-5:] != ".mark" and n[-3:] != ".sa" and n[-3:] != ".sv":
  329. return "", "", 0,
  330. s = n.split(".")
  331. i = int(s[0])
  332. return s[2], s[1], i
  333. def help(m):
  334. print("mpegize command base-dir destination-movie fps")
  335. def run(m, argv):
  336. if len(argv) > 2:
  337. m.base = argv[2]
  338. mode = argv[1]
  339. if mode == "convert":
  340. print("Phase 1: jpegize")
  341. print("Message: 0%%")
  342. sys.stdout.flush()
  343. m.prepare()
  344. m.jpegize()
  345. print("Phase 2: mpegize -- ", argv[3])
  346. print("Message: enc")
  347. sys.stdout.flush()
  348. gst_convert(m.base, argv[3], True)
  349. return
  350. if mode == "gaps":
  351. print("Video gaps")
  352. m.stat_gaps("sv")
  353. print("Audio gaps")
  354. m.stat_gaps("sa")
  355. return
  356. if mode == "jpegize":
  357. m.prepare()
  358. m.jpegize()
  359. return
  360. m.help()
  361. def stat_gaps(m, e):
  362. m.prepare_source()
  363. last = 0
  364. num = 0
  365. total = 0
  366. limit = 1000000 / m.fps + 15000
  367. for n in m.frames:
  368. ext, mid, i = m.parse_frame(n)
  369. if ext != e:
  370. continue
  371. if i - last > limit:
  372. print("Gap at", i, (i - last) / 1000., "msec")
  373. num += 1
  374. last = i
  375. total += 1
  376. print("Total", num, "gaps of", total)
  377. print("Expected", (1000000 / m.fps) / 1000., "msec, limit", limit / 1000., "msec")
  378. def display_usec(m, v):
  379. return "%.2f sec" % (v/1000000.)
  380. def display_frames(m, v):
  381. return "%d frames %s" % (v, m.display_usec(v * 1000000 / 30.))
  382. def jpegize(m):
  383. i = 0
  384. os.chdir(m.base)
  385. l = os.listdir(m.base)
  386. l = filter(lambda n: n[-4:] == ".dng", l)
  387. l = list(l)
  388. l.sort()
  389. print("Have", m.display_frames(len(l)), "dngs")
  390. for n in l:
  391. if n[-4:] != ".dng":
  392. print("Something went terribly wrong")
  393. continue
  394. i += 1
  395. print("Message: %.0f%%" % ((100*i) / len(l)))
  396. sys.stdout.flush()
  397. base = n[:-4]
  398. subprocess.run(['dcraw',
  399. '-w', # -w Use camera white balance
  400. '+M', # +M use embedded color matrix
  401. '-H', '2', # -H 2 Recover highlights by blending them
  402. '-o', '1', # -o 1 Output in sRGB colorspace
  403. '-q', '0', # -q 0 Debayer with fast bi-linear interpolation
  404. '-f', # -f Interpolate RGGB as four colors
  405. '-T', n]) # -T Output TIFF
  406. subprocess.run(['convert', base+'.tiff', base+'.jpeg'])
  407. os.unlink(base+'.tiff')
  408. os.rename(base+'.jpeg', m.source+"/"+base+'.jpeg.sv')
  409. m = Mpegize()
  410. m.run(sys.argv)