mpegize.py 14 KB

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