mpegize.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #!/usr/bin/python3
  2. # Copyright 2022, 2024 Pavel Machek, GPLv2+
  3. import os, sys, time, copy, subprocess
  4. # https://stackoverflow.com/questions/11779490/how-to-add-a-new-audio-not-mixing-into-a-video-using-ffmpeg
  5. # https://ottverse.com/create-video-from-images-using-ffmpeg/
  6. # https://github.com/kkroening/ffmpeg-python/issues/95
  7. # sudo apt install ffmpeg
  8. # Usage: mpegize convert
  9. # head -c 1000 < /dev/zero > /tmp/delme.sm/1.foo.sa
  10. class Mpegize:
  11. base = '/tmp/delme.'
  12. fps = 30.5
  13. def prepare(m):
  14. m.source = m.base+'sm'
  15. m.work = m.base+'smt'
  16. m.output = m.base+'smo'
  17. def prepare_work(m):
  18. m.prepare()
  19. if not os.path.exists(m.output):
  20. os.mkdir(m.output)
  21. if not os.path.exists(m.work):
  22. os.mkdir(m.work)
  23. os.chdir(m.work)
  24. os.system("rm *.jpeg output.*")
  25. def prepare_source(m):
  26. m.prepare()
  27. m.out_index = 0
  28. l = os.listdir(m.source)
  29. print("Have", m.display_frames(len(l)), "frames")
  30. l.sort()
  31. m.frames = l
  32. m.unused_frames = copy.deepcopy(l)
  33. def parse_frame(m, n):
  34. if n[-5:] != ".mark" and n[-3:] != ".sa" and n[-3:] != ".sv":
  35. return "", "", 0,
  36. s = n.split(".")
  37. i = int(s[0])
  38. return s[2], s[1], i
  39. def help(m):
  40. print("mpegize command base-dir")
  41. def run(m, argv):
  42. if len(argv) > 2:
  43. m.base = argv[2]
  44. mode = argv[1]
  45. if mode == "stat" or mode == "convert" or mode == "gc" or mode == "convertall":
  46. m.process(mode)
  47. return
  48. if mode == "gaps":
  49. print("Video gaps")
  50. m.stat_gaps("sv")
  51. print("Audio gaps")
  52. m.stat_gaps("sa")
  53. return
  54. if mode == "jpegize":
  55. m.prepare()
  56. m.jpegize()
  57. return
  58. m.help()
  59. def stat_gaps(m, e):
  60. m.prepare_source()
  61. last = 0
  62. num = 0
  63. total = 0
  64. limit = 1000000 / m.fps + 15000
  65. for n in m.frames:
  66. ext, mid, i = m.parse_frame(n)
  67. if ext != e:
  68. continue
  69. if i - last > limit:
  70. print("Gap at", i, (i - last) / 1000., "msec")
  71. num += 1
  72. last = i
  73. total += 1
  74. print("Total", num, "gaps of", total)
  75. print("Expected", (1000000 / m.fps) / 1000., "msec, limit", limit / 1000., "msec")
  76. def process(m, mode):
  77. m.prepare_source()
  78. photos = 0
  79. video_frames = 0
  80. start = 0
  81. for n in m.frames:
  82. ext, mid, i = m.parse_frame(n)
  83. if ext != "mark":
  84. continue
  85. print(n)
  86. if mid == "start":
  87. start = i
  88. if mid == "stop":
  89. video_frames += m.extract_video(start, i, mode)
  90. start = 0
  91. if mid == "wow":
  92. if start:
  93. start -= 5000000
  94. else:
  95. photos += 5
  96. m.extract_photo(i - 1000000, mode)
  97. m.extract_photo(i - 2000000, mode)
  98. m.extract_photo(i - 3000000, mode)
  99. m.extract_photo(i - 4000000, mode)
  100. m.extract_photo(i - 5000000, mode)
  101. if mid == "photo":
  102. photos += 1
  103. m.extract_photo(i, mode)
  104. if mode == "convertall":
  105. video_frames += m.extract_video(0, 9999999999999999, "convert")
  106. return
  107. print("Total", photos, "photos and", m.display_frames(video_frames))
  108. print(len(m.unused_frames), "/", len(m.frames))
  109. if mode == "gc":
  110. os.chdir(m.source)
  111. for n in m.unused_frames:
  112. os.unlink(n)
  113. print(m.unused_frames)
  114. def display_usec(m, v):
  115. return "%.2f sec" % (v/1000000.)
  116. def display_frames(m, v):
  117. return "%d frames %s" % (v, m.display_usec(v * 1000000 / 30.))
  118. def frame_used(m, n):
  119. if n in m.unused_frames:
  120. m.unused_frames.remove(n)
  121. def extract_photo(m, around, mode):
  122. print("Photo:", around)
  123. best = None
  124. for n in m.frames:
  125. ext, mid, i = m.parse_frame(n)
  126. if ext != "sv":
  127. continue
  128. if i < around:
  129. best = n
  130. continue
  131. best = n
  132. break
  133. m.frame_used(best)
  134. out_file = m.output+"/image-%04d.jpeg" % m.out_index
  135. m.out_index += 1
  136. if mode == "convert":
  137. os.system("ln "+m.source+"/"+best+" "+out_file)
  138. def extract_video(m, start, end, mode):
  139. print("Searching video", start, end, "--", m.display_usec(end-start))
  140. if mode == "convert":
  141. m.prepare_work()
  142. t1 = time.time()
  143. seen_audio = False
  144. seen_video = False
  145. count = 0
  146. skip_audio = 0
  147. skip_video = 0
  148. num = 0
  149. for n in m.frames:
  150. num += 1
  151. if not num % 1000:
  152. print("Frame", num)
  153. ext, mid, i = m.parse_frame(n)
  154. if ext != "sa" and ext != "sv":
  155. m.frame_used(n)
  156. continue
  157. if i < start - 1000000 or i > end:
  158. continue
  159. if ext == "sa":
  160. seen_audio = True
  161. if not seen_video:
  162. continue
  163. if mode == "convert":
  164. os.system("cat "+m.source+"/"+n+" >> "+m.work+"/output.raw")
  165. if ext == "sv":
  166. if not seen_video:
  167. first_video = i
  168. seen_video = True
  169. if mode == "convert":
  170. os.system("ln "+m.source+"/"+n+" "+m.work+"/image-%06d.jpeg" % count)
  171. count += 1
  172. while i >= first_video + count * 1000000 / m.fps:
  173. print("Duplicating video frame at", i)
  174. if mode == "convert":
  175. os.system("ln "+m.source+"/"+n+" "+m.work+"/image-%06d.jpeg" % count)
  176. count += 1
  177. m.frame_used(n)
  178. if mode == "convert":
  179. os.chdir(m.work)
  180. print("Converting", m.display_frames(count), "skipped", skip_audio, "audio and", skip_video, "video frames")
  181. os.system("ffmpeg -f s16le -ac 2 -ar 48000 -i output.raw output.wav")
  182. options = "-b:v 4096k -c:v libx264 -preset ultrafast"
  183. os.system("ffmpeg -framerate %d -i image-%%06d.jpeg -i output.wav %s output.mp4" % (m.fps, options))
  184. os.system("rm output.raw")
  185. out_file = m.output+"/video-%04d.mp4" % m.out_index
  186. m.out_index += 1
  187. os.system("mv output.mp4 "+out_file)
  188. print("Converted", m.display_frames(count), "in", "%.1f" % (time.time()-t1), "seconds")
  189. if mode == "convert":
  190. print("Original size -> new size")
  191. os.system("du -sh .; du -sh "+out_file)
  192. return count
  193. def jpegize(m):
  194. i = 0
  195. os.chdir(m.base)
  196. l = os.listdir(m.base)
  197. l = filter(lambda n: n[-4:] == ".dng", l)
  198. l = list(l)
  199. l.sort()
  200. print("Have", m.display_frames(len(l)), "dngs")
  201. for n in l:
  202. if n[-4:] != ".dng":
  203. print("Something went terribly wrong")
  204. continue
  205. i += 1
  206. print(i, '/', len(l))
  207. base = n[:-4]
  208. subprocess.run(['dcraw',
  209. '-w', # -w Use camera white balance
  210. '+M', # +M use embedded color matrix
  211. '-H', '2', # -H 2 Recover highlights by blending them
  212. '-o', '1', # -o 1 Output in sRGB colorspace
  213. '-q', '0', # -q 0 Debayer with fast bi-linear interpolation
  214. '-f', # -f Interpolate RGGB as four colors
  215. '-T', n]) # -T Output TIFF
  216. subprocess.run(['convert', base+'.tiff', base+'.jpeg'])
  217. os.unlink(base+'.tiff')
  218. os.rename(base+'.jpeg', m.source+"/"+base+'.jpeg.sv')
  219. m = Mpegize()
  220. m.run(sys.argv)