#!/usr/bin/python3 # Copyright 2022, 2024 Pavel Machek, GPLv2+ import os, sys, time, copy, subprocess import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GLib import os import time # https://stackoverflow.com/questions/11779490/how-to-add-a-new-audio-not-mixing-into-a-video-using-ffmpeg # https://ottverse.com/create-video-from-images-using-ffmpeg/ # https://github.com/kkroening/ffmpeg-python/issues/95 # sudo apt install ffmpeg # Usage: mpegize convert # head -c 1000 < /dev/zero > /tmp/delme.sm/1.foo.sa def gst_convert(mega_dir, out_file, use_jpeg): def sa_read(name, t): with open(name, "rb") as file: rgb_data = file.read(10*1024*1024) caps_string = "audio/x-raw,format=U16LE,channels=2,rate=48000,layout=interleaved,channel-mask=3" caps = Gst.Caps.from_string(caps_string) buffer = Gst.Buffer.new_wrapped(rgb_data) if False: time.sleep(1/30.) # nanoseconds buffer.pts = time.time() * 1000*1000*1000 buffer.dts = time.time() * 1000*1000*1000 elif True: buffer.pts = t buffer.dts = t buffer.duration = (1000*1000*1000)/10. return buffer, caps def sa_src(appsrc): def on_need_data(appsrc, data, length): name = audio.get_path() if name == None or name[-22:] != ".48000-s16le-stereo.sa": appsrc.emit("end-of-stream") print("End of audio stream") return t = audio.get_time() #print("Audio: ", name, " need ", data, t) buffer, caps = sa_read(name, t) os.unlink(name) appsrc.set_property("caps", caps) appsrc.emit("push-buffer", buffer) appsrc.set_property("format", Gst.Format.TIME) appsrc.set_property("is-live", False) appsrc.set_property("block", True) name = audio.get_path() buffer, caps = sa_read(name, 0) appsrc.set_property("caps", caps) #appsrc.emit("push-buffer", buffer) s = appsrc.connect("need-data", on_need_data, "") print("Connect", s) class grwBase: def init(m, dir): m.dir = dir m.slen = len(m.suffix) m.start_time = 0 m.scan() print("Movie", len(m.list)) def scan(m): m.list = os.listdir(m.dir) m.list.sort() m.length = len(m.list) def get_path(m): s = m.get_name() if s: return m.dir + s return s def get_name(m): m.scan() #print("Get path -- ") while True: if (len(m.list)) == 0: return None #print("Get path: ", m.list[0], m.suffix) if m.list[0][-m.slen:] != m.suffix: m.pop() continue return m.list[0] def get_time(m): s = m.get_name() s = s[:-m.slen] t = int(s) res = t * 1000 - m.start_time t = t / (1000*1000.) while (time.time() - t < 1): print("Too fast: ", time.time(), t, file=sys.stderr) print("Message: WA") sys.stdout.flush() time.sleep(.1) return res def pop(m): m.list = m.list[1:] def progress(m): i = len(m.list) print("Message: %d" % i) sys.stdout.flush() class grwVideo(grwBase): suffix = ".grw" def __init__(m, dir): m.init(dir) class grwJPEG(grwBase): suffix = ".jpeg.sv" def __init__(m, dir): m.init(dir + "sm/") class grwAudio(grwVideo): suffix = ".48000-s16le-stereo.sa" def __init__(m, dir): m.init(dir + "sm/") def grw_read(name, t): with open(name, "rb") as file: rgb_data = file.read(10*1024*1024) i = len(rgb_data) i -= 1 while rgb_data[i] != 0: i -= 1 footer = rgb_data[i+1:] sp = str(footer, 'ascii').split('\n') # Create caps for the file caps_string = sp[0][6:] caps = Gst.Caps.from_string(caps_string) if sp[0][:6] != "Caps: ": print("Bad footer") if sp[1][:6] != "Size: ": print("Bad footer") if sp[-1] != "GRW": print("Missing GRW footer") buffer = Gst.Buffer.new_wrapped(rgb_data) # This does not work for interactive use. if False: time.sleep(1/30.) # nanoseconds buffer.pts = time.time() * 1000*1000*1000 buffer.dts = time.time() * 1000*1000*1000 elif True: buffer.pts = t buffer.dts = t buffer.duration = (1000*1000*1000)/30. return buffer, caps def grwsrc(appsrc): def on_need_data(appsrc, data, length): name = movie.get_path() if name == None or name[-4:] != ".grw": appsrc.emit("end-of-stream") print("End of video stream") return t = movie.get_time() #print("Video: ", name, t) movie.progress() buffer, caps = grw_read(name, t) os.unlink(name) appsrc.set_property("caps", caps) appsrc.emit("push-buffer", buffer) appsrc.set_property("format", Gst.Format.TIME) appsrc.set_property("is-live", False) appsrc.set_property("block", True) name = movie.get_path() buffer, caps = grw_read(name, 0) appsrc.set_property("caps", caps) #appsrc.emit("push-buffer", buffer) s = appsrc.connect("need-data", on_need_data, "") print("Connect", s) def jpeg_read(name, t): with open(name, "rb") as file: rgb_data = file.read(10*1024*1024) i = len(rgb_data) buffer = Gst.Buffer.new_wrapped(rgb_data) caps_string = "image/jpeg" caps = Gst.Caps.from_string(caps_string) # This does not work for interactive use. if False: time.sleep(1/30.) # nanoseconds buffer.pts = time.time() * 1000*1000*1000 buffer.dts = time.time() * 1000*1000*1000 elif True: buffer.pts = t buffer.dts = t buffer.duration = (1000*1000*1000)/30. return buffer, caps def jpeg_src(appsrc): def on_need_data(appsrc, data, length): name = movie.get_path() if name == None or name[-8:] != ".jpeg.sv": appsrc.emit("end-of-stream") print("End of video stream") return t = movie.get_time() #print("Video: ", name, t) buffer, caps = jpeg_read(name, t) os.unlink(name) appsrc.set_property("caps", caps) appsrc.emit("push-buffer", buffer) appsrc.set_property("format", Gst.Format.TIME) appsrc.set_property("is-live", False) appsrc.set_property("block", True) name = movie.get_path() buffer, caps = jpeg_read(name, 0) appsrc.set_property("caps", caps) #appsrc.emit("push-buffer", buffer) s = appsrc.connect("need-data", on_need_data, "") print("Connect", s) def v_src(appsrc): if not use_jpeg: grwsrc(appsrc) else: jpeg_src(appsrc) count = 0 path = mega_dir if use_jpeg: movie = grwJPEG(path) else: movie = grwVideo(path) audio = grwAudio(path) t1 = movie.get_time() t2 = audio.get_time() tm = min(t1,t2) print("Time base is", tm) movie.start_time = tm audio.start_time = tm def pipeline_video(): if True: s = "appsrc name=source" if use_jpeg: s += " ! jpegdec " else: s = "videotestsrc" s += " ! video/x-raw,width=(int)640,height=(int)480,format=(string)RGB " if False: s += " ! videoconvert ! jpegenc" s += " ! appsink name=sink" elif True: s += " ! videoconvert ! autovideosink" else: s += " ! videoconvert ! x264enc bitrate=3072 speed-preset=ultrafast ! matroskamux ! filesink location=" + out_file pipeline = Gst.parse_launch(s) p = pipeline.get_by_name("source") if p: if False: mysrc(p) else: v_src(p) p = pipeline.get_by_name("sink") if p: mysink(p) return pipeline def pipeline_audio(): # audiotestsrc ! audioconvert ! audioresample ! autoaudiosink if True: s = "appsrc name=source" else: s = "audiotestsrc" if True: s += " ! audiobuffersplit ! audioconvert ! audioresample ! autoaudiosink" else: s += " ! ! ! " pipeline = Gst.parse_launch(s) p = pipeline.get_by_name("source") if p: sa_src(p) p = pipeline.get_by_name("sink") if p: mysink(p) return pipeline def pipeline_both(): if True: s = "appsrc name=asrc" else: s = "audiotestsrc" # Audiobuffersplit creates problems with A/V synchronization, avoid. #s += "! audiobuffersplit" s += " ! audioconvert ! vorbisenc ! mux. " if True: s += "appsrc name=vsrc" if use_jpeg: s += " ! jpegdec " else: s += "videotestsrc" s += " ! video/x-raw,width=(int)640,height=(int)480,format=(string)RGB " s += " ! videoconvert ! x264enc bitrate=3072 speed-preset=ultrafast ! matroskamux name=mux" if False: s += " ! decodebin ! playsink" else: s += " ! filesink location="+out_file pipeline = Gst.parse_launch(s) p = pipeline.get_by_name("asrc") if p: sa_src(p) p = pipeline.get_by_name("vsrc") if p: v_src(p) return pipeline Gst.init(None) Gst.debug_set_default_threshold(Gst.DebugLevel.WARNING) if False: Gst.debug_set_default_threshold(Gst.DebugLevel.INFO) if False: pipeline = pipeline_video() elif False: pipeline = pipeline_audio() else: pipeline = pipeline_both() # Function to handle end of stream def on_eos(bus, message): print("End of stream") pipeline.set_state(Gst.State.NULL) loop.quit() # Set up bus to handle messages bus = pipeline.get_bus() bus.add_signal_watch() bus.connect("message::eos", on_eos) # Set the pipeline to the playing state pipeline.set_state(Gst.State.PLAYING) # Run the main loop to handle GStreamer events loop = GLib.MainLoop() try: loop.run() except KeyboardInterrupt: pipeline.set_state(Gst.State.NULL) loop.quit() class Mpegize: base = '/tmp/delme.' fps = 30.5 def prepare(m): m.source = m.base+'sm' m.work = m.base+'smt' m.output = m.base+'smo' def prepare_work(m): m.prepare() if not os.path.exists(m.output): os.mkdir(m.output) if not os.path.exists(m.work): os.mkdir(m.work) os.chdir(m.work) os.system("rm *.jpeg output.*") def prepare_source(m): m.prepare() m.out_index = 0 l = os.listdir(m.source) print("Have", m.display_frames(len(l)), "frames") l.sort() m.frames = l m.unused_frames = copy.deepcopy(l) def parse_frame(m, n): if n[-5:] != ".mark" and n[-3:] != ".sa" and n[-3:] != ".sv": return "", "", 0, s = n.split(".") i = int(s[0]) return s[2], s[1], i def help(m): print("mpegize command base-dir destination-movie fps dng|grw") def cleanup(m): os.system("rmdir %s/sm/" % m.base) os.system("rmdir %s/" % m.base) print("Message: Rec") sys.stdout.flush() def run(m, argv): if len(argv) > 2: m.base = argv[2] mode = argv[1] fps = argv[4] ext = argv[5] if mode == "start": print("Phase 0: start, mode ", ext, file=sys.stderr) if ext!="grw": return print("Phase 0: wait", file=sys.stderr) print("Message: W1") sys.stdout.flush() time.sleep(1) print("Phase 1: parallel fun", file=sys.stderr) print("Message: proc") sys.stdout.flush() gst_convert(m.base, argv[3], argv[4]=="dng") m.cleanup() return if mode == "convert" or mode == "stop": if ext=="grw": return print("Phase 1: jpegize", file=sys.stderr) print("Message: 0%%") sys.stdout.flush() m.prepare() m.jpegize() print("Phase 2: mpegize -- ", argv[3], file=sys.stderr) print("Message: enc") sys.stdout.flush() gst_convert(m.base, argv[3], argv[4]=="dng") m.cleanup() return if mode == "gaps": print("Video gaps") m.stat_gaps("sv") print("Audio gaps") m.stat_gaps("sa") return if mode == "jpegize": m.prepare() m.jpegize() return m.help() def stat_gaps(m, e): m.prepare_source() last = 0 num = 0 total = 0 limit = 1000000 / m.fps + 15000 for n in m.frames: ext, mid, i = m.parse_frame(n) if ext != e: continue if i - last > limit: print("Gap at", i, (i - last) / 1000., "msec") num += 1 last = i total += 1 print("Total", num, "gaps of", total) print("Expected", (1000000 / m.fps) / 1000., "msec, limit", limit / 1000., "msec") def display_usec(m, v): return "%.2f sec" % (v/1000000.) def display_frames(m, v): return "%d frames %s" % (v, m.display_usec(v * 1000000 / 30.)) def jpegize(m): i = 0 os.chdir(m.base) l = os.listdir(m.base) l = filter(lambda n: n[-4:] == ".dng", l) l = list(l) l.sort() print("Have", m.display_frames(len(l)), "dngs") for n in l: if n[-4:] != ".dng": print("Something went terribly wrong") continue i += 1 print("Message: %.0f%%" % ((100*i) / len(l))) sys.stdout.flush() base = n[:-4] subprocess.run(['dcraw', '-w', # -w Use camera white balance '+M', # +M use embedded color matrix '-H', '2', # -H 2 Recover highlights by blending them '-o', '1', # -o 1 Output in sRGB colorspace '-q', '0', # -q 0 Debayer with fast bi-linear interpolation '-f', # -f Interpolate RGGB as four colors '-T', n]) # -T Output TIFF subprocess.run(['convert', base+'.tiff', base+'.jpeg']) os.unlink(base+'.tiff') os.rename(base+'.jpeg', m.source+"/"+base+'.jpeg.sv') m = Mpegize() m.run(sys.argv)