Add support files that will be used for video recording.
This commit is contained in:

committed by
Martijn Braam

parent
b23ec208f0
commit
fcba2b33c2
14
medianame.h
Normal file
14
medianame.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
static inline unsigned long long time_usec(void)
|
||||||
|
{
|
||||||
|
struct timeval t;
|
||||||
|
gettimeofday(&t, NULL);
|
||||||
|
|
||||||
|
return t.tv_sec * 1000000 + t.tv_usec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void get_name(char *buf, char *dir, char *templ)
|
||||||
|
{
|
||||||
|
sprintf(buf, "%s/%lld.%s", dir, time_usec(), templ);
|
||||||
|
}
|
22
meson.build
22
meson.build
@@ -68,6 +68,28 @@ executable('megapixels',
|
|||||||
install: true,
|
install: true,
|
||||||
link_args: '-Wl,-ldl')
|
link_args: '-Wl,-ldl')
|
||||||
|
|
||||||
|
executable('movie_audio_rec',
|
||||||
|
'movie_audio_rec.c',
|
||||||
|
dependencies: [ dependency('libpulse-simple') ],
|
||||||
|
install : true,
|
||||||
|
install_dir: get_option('libexecdir') / 'megapixels/',
|
||||||
|
)
|
||||||
|
|
||||||
|
install_data(
|
||||||
|
[
|
||||||
|
'mpegize.py'
|
||||||
|
],
|
||||||
|
install_dir: get_option('libexecdir') / 'megapixels/',
|
||||||
|
)
|
||||||
|
|
||||||
|
configure_file(
|
||||||
|
input: 'movie.sh.in',
|
||||||
|
output: 'movie.sh',
|
||||||
|
configuration: {'LIBEXECDIR': join_paths(get_option('prefix'), get_option('libexecdir')) / 'megapixels/'},
|
||||||
|
install_dir: get_option('datadir') / 'megapixels/',
|
||||||
|
install_mode: 'rwxr-xr-x',
|
||||||
|
)
|
||||||
|
|
||||||
install_data(
|
install_data(
|
||||||
[
|
[
|
||||||
'config/pine64,pinephone,rear.dcp',
|
'config/pine64,pinephone,rear.dcp',
|
||||||
|
76
movie.sh.in
Executable file
76
movie.sh.in
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2022 Pavel Machek, GPLv2+
|
||||||
|
|
||||||
|
# needs sudo apt install dcraw
|
||||||
|
|
||||||
|
jpegize() {
|
||||||
|
DNG_DIR="$1"
|
||||||
|
BURST_DIR="$GIGA_DIR/sm/"
|
||||||
|
mkdir $BURST_DIR
|
||||||
|
|
||||||
|
DCRAW=dcraw
|
||||||
|
TIFF_EXT="tiff"
|
||||||
|
set --
|
||||||
|
|
||||||
|
CONVERT="convert"
|
||||||
|
|
||||||
|
cd $DNG_DIR
|
||||||
|
I=0
|
||||||
|
NUM=0
|
||||||
|
for DNG in *.dng; do
|
||||||
|
NUM=$[$NUM+1]
|
||||||
|
done
|
||||||
|
|
||||||
|
for DNG in *.dng; do
|
||||||
|
PERC=$[(100*$I)/$NUM]
|
||||||
|
echo $PERC
|
||||||
|
BASE=${DNG%%.dng}
|
||||||
|
# -w Use camera white balance
|
||||||
|
# +M use embedded color matrix
|
||||||
|
# -H 2 Recover highlights by blending them
|
||||||
|
# -o 1 Output in sRGB colorspace
|
||||||
|
# -q 0 Debayer with fast bi-linear interpolation
|
||||||
|
# -f Interpolate RGGB as four colors
|
||||||
|
# -T Output TIFF
|
||||||
|
(
|
||||||
|
$DCRAW -w +M -H 2 -o 1 -q 0 -f -T "$DNG"
|
||||||
|
$CONVERT "$BASE.tiff" "$BASE.jpeg"
|
||||||
|
rm "$BASE.tiff"
|
||||||
|
mv "$BASE.jpeg" "$BURST_DIR/$BASE.jpeg.sv"
|
||||||
|
) &
|
||||||
|
# dcraw -h -> half size -- fast!
|
||||||
|
# can do ppm output
|
||||||
|
I=$[$I+1]
|
||||||
|
if [ 0 == $[ $I % 16 ] ]; then
|
||||||
|
echo "Batch $I -- $PERC %" 1>&2
|
||||||
|
wait
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
|
||||||
|
GIGA_DIR="$2"
|
||||||
|
DEST_NAME="$3"
|
||||||
|
FPS="$4"
|
||||||
|
|
||||||
|
echo script_dir $SCRIPT_DIR 1>&2
|
||||||
|
echo GIGA_DIR $GIGA_DIR 1>&2
|
||||||
|
echo DEST_NAME $DEST_NAME 1>&2
|
||||||
|
echo FPS $FPS dfps 1>&2
|
||||||
|
|
||||||
|
if [ "-$1" == "-start" ]; then
|
||||||
|
mkdir $GIGA_DIR/sm
|
||||||
|
cd $GIGA_DIR/sm
|
||||||
|
@LIBEXECDIR@/movie_audio_rec $FPS &
|
||||||
|
echo $! > $2/audio.pid
|
||||||
|
elif [ "-$1" == "-stop" ]; then
|
||||||
|
mkdir $GIGA_DIR/sm
|
||||||
|
kill `cat $2/audio.pid`
|
||||||
|
jpegize $2 # | zenity --progress "--text=Converting, phase 1, dng -> jpeg" --time-remaining
|
||||||
|
cd $GIGA_DIR/sm
|
||||||
|
@LIBEXECDIR@/mpegize.py convertall $GIGA_DIR/ $FPS
|
||||||
|
mv $GIGA_DIR/smo/*.mp4 $DEST_NAME
|
||||||
|
rm -r $GIGA_DIR
|
||||||
|
else
|
||||||
|
echo "Unrecognized command"
|
||||||
|
fi
|
93
movie_audio_rec.c
Normal file
93
movie_audio_rec.c
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/* -*- c-file-style: "linux" -*- */
|
||||||
|
/***
|
||||||
|
This file is part of PulseAudio.
|
||||||
|
PulseAudio is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published
|
||||||
|
by the Free Software Foundation; either version 2.1 of the License,
|
||||||
|
or (at your option) any later version.
|
||||||
|
PulseAudio is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
* Copyright 2022, 2024 Pavel Machek
|
||||||
|
***/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pulse/simple.h>
|
||||||
|
#include <pulse/error.h>
|
||||||
|
|
||||||
|
#include "medianame.h"
|
||||||
|
|
||||||
|
/* gcc xx.c -o xx $(pkg-config --cflags --libs libpulse-simple)
|
||||||
|
*/
|
||||||
|
|
||||||
|
int main(int argc, char*argv[]) {
|
||||||
|
int fps = 305; /* fps * 10 */
|
||||||
|
/* 48000 * 2 * 2 bps, we want chunks corresponding to 30 fps */
|
||||||
|
const uint32_t bufsize = ((48000 * 2 * 2 * 10) / fps);
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
printf("usage: prog fps*10, run in recording directory\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
fps = atoi(argv[1]);
|
||||||
|
|
||||||
|
/* The sample type to use */
|
||||||
|
static const pa_sample_spec ss = {
|
||||||
|
.format = PA_SAMPLE_S16LE,
|
||||||
|
.rate = 48000,
|
||||||
|
.channels = 2
|
||||||
|
};
|
||||||
|
static pa_buffer_attr attr = {
|
||||||
|
.minreq = (uint32_t) -1,
|
||||||
|
.prebuf = (uint32_t) -1,
|
||||||
|
.tlength = (uint32_t) -1,
|
||||||
|
};
|
||||||
|
pa_simple *r = NULL;
|
||||||
|
int ret = 1;
|
||||||
|
int error;
|
||||||
|
const pa_buffer_attr *p_attr = &attr;
|
||||||
|
int opt = 0; // | PA_STREAM_ADJUST_LATENCY;
|
||||||
|
uint8_t *buf = malloc(bufsize);
|
||||||
|
|
||||||
|
attr.fragsize = bufsize;
|
||||||
|
attr.maxlength = bufsize;
|
||||||
|
|
||||||
|
/* Create the recording stream */
|
||||||
|
if (!(r = pa_simple_new(NULL, argv[0], PA_STREAM_RECORD | opt, NULL, "record", &ss, NULL, p_attr, &error))) {
|
||||||
|
fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
char name[1024];
|
||||||
|
int fd, res;
|
||||||
|
|
||||||
|
/* Record some data ... */
|
||||||
|
if (pa_simple_read(r, buf, bufsize, &error) < 0) {
|
||||||
|
fprintf(stderr, __FILE__": pa_simple_read() failed: %s\n", pa_strerror(error));
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_name(name, ".", "44800-s16le-stereo.sa");
|
||||||
|
fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0666);
|
||||||
|
res = write(fd, buf, bufsize);
|
||||||
|
if (res != bufsize) {
|
||||||
|
fprintf(stderr, __FILE__": could not write samples: %m\n");
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
finish:
|
||||||
|
if (r)
|
||||||
|
pa_simple_free(r);
|
||||||
|
return ret;
|
||||||
|
}
|
241
mpegize.py
Executable file
241
mpegize.py
Executable file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# Copyright 2022 Pavel Machek, GPLv2+
|
||||||
|
|
||||||
|
import os, sys, time, copy, subprocess
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
def run(m, argv):
|
||||||
|
if len(argv) > 2:
|
||||||
|
m.base = argv[2]
|
||||||
|
mode = argv[1]
|
||||||
|
if mode == "stat" or mode == "convert" or mode == "gc" or mode == "convertall":
|
||||||
|
m.process(mode)
|
||||||
|
return
|
||||||
|
if mode == "gaps":
|
||||||
|
print("Video gaps")
|
||||||
|
m.stat_gaps("sv")
|
||||||
|
print("Audio gaps")
|
||||||
|
m.stat_gaps("sa")
|
||||||
|
return
|
||||||
|
if mode == "jpegize":
|
||||||
|
m.jpegize()
|
||||||
|
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 process(m, mode):
|
||||||
|
m.prepare_source()
|
||||||
|
photos = 0
|
||||||
|
video_frames = 0
|
||||||
|
start = 0
|
||||||
|
for n in m.frames:
|
||||||
|
ext, mid, i = m.parse_frame(n)
|
||||||
|
if ext != "mark":
|
||||||
|
continue
|
||||||
|
print(n)
|
||||||
|
if mid == "start":
|
||||||
|
start = i
|
||||||
|
if mid == "stop":
|
||||||
|
video_frames += m.extract_video(start, i, mode)
|
||||||
|
start = 0
|
||||||
|
if mid == "wow":
|
||||||
|
if start:
|
||||||
|
start -= 5000000
|
||||||
|
else:
|
||||||
|
photos += 5
|
||||||
|
m.extract_photo(i - 1000000, mode)
|
||||||
|
m.extract_photo(i - 2000000, mode)
|
||||||
|
m.extract_photo(i - 3000000, mode)
|
||||||
|
m.extract_photo(i - 4000000, mode)
|
||||||
|
m.extract_photo(i - 5000000, mode)
|
||||||
|
if mid == "photo":
|
||||||
|
photos += 1
|
||||||
|
m.extract_photo(i, mode)
|
||||||
|
if mode == "convertall":
|
||||||
|
video_frames += m.extract_video(0, 9999999999999999, "convert")
|
||||||
|
return
|
||||||
|
print("Total", photos, "photos and", m.display_frames(video_frames))
|
||||||
|
print(len(m.unused_frames), "/", len(m.frames))
|
||||||
|
if mode == "gc":
|
||||||
|
os.chdir(m.source)
|
||||||
|
for n in m.unused_frames:
|
||||||
|
os.unlink(n)
|
||||||
|
print(m.unused_frames)
|
||||||
|
|
||||||
|
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 frame_used(m, n):
|
||||||
|
if n in m.unused_frames:
|
||||||
|
m.unused_frames.remove(n)
|
||||||
|
|
||||||
|
def extract_photo(m, around, mode):
|
||||||
|
print("Photo:", around)
|
||||||
|
best = None
|
||||||
|
for n in m.frames:
|
||||||
|
ext, mid, i = m.parse_frame(n)
|
||||||
|
if ext != "sv":
|
||||||
|
continue
|
||||||
|
if i < around:
|
||||||
|
best = n
|
||||||
|
continue
|
||||||
|
best = n
|
||||||
|
break
|
||||||
|
|
||||||
|
m.frame_used(best)
|
||||||
|
out_file = m.output+"/image-%04d.jpeg" % m.out_index
|
||||||
|
m.out_index += 1
|
||||||
|
if mode == "convert":
|
||||||
|
os.system("ln "+m.source+"/"+best+" "+out_file)
|
||||||
|
|
||||||
|
def extract_video(m, start, end, mode):
|
||||||
|
print("Searching video", start, end, "--", m.display_usec(end-start))
|
||||||
|
if mode == "convert":
|
||||||
|
m.prepare_work()
|
||||||
|
t1 = time.time()
|
||||||
|
seen_audio = False
|
||||||
|
seen_video = False
|
||||||
|
count = 0
|
||||||
|
skip_audio = 0
|
||||||
|
skip_video = 0
|
||||||
|
num = 0
|
||||||
|
for n in m.frames:
|
||||||
|
num += 1
|
||||||
|
if not num % 1000:
|
||||||
|
print("Frame", num)
|
||||||
|
ext, mid, i = m.parse_frame(n)
|
||||||
|
if ext != "sa" and ext != "sv":
|
||||||
|
m.frame_used(n)
|
||||||
|
continue
|
||||||
|
if i < start - 1000000 or i > end:
|
||||||
|
continue
|
||||||
|
if ext == "sa":
|
||||||
|
seen_audio = True
|
||||||
|
if not seen_video:
|
||||||
|
continue
|
||||||
|
if mode == "convert":
|
||||||
|
os.system("cat "+m.source+"/"+n+" >> "+m.work+"/output.raw")
|
||||||
|
if ext == "sv":
|
||||||
|
if not seen_video:
|
||||||
|
first_video = i
|
||||||
|
seen_video = True
|
||||||
|
if mode == "convert":
|
||||||
|
os.system("ln "+m.source+"/"+n+" "+m.work+"/image-%06d.jpeg" % count)
|
||||||
|
count += 1
|
||||||
|
while i >= first_video + count * 1000000 / m.fps:
|
||||||
|
print("Duplicating video frame at", i)
|
||||||
|
if mode == "convert":
|
||||||
|
os.system("ln "+m.source+"/"+n+" "+m.work+"/image-%06d.jpeg" % count)
|
||||||
|
count += 1
|
||||||
|
m.frame_used(n)
|
||||||
|
|
||||||
|
if mode == "convert":
|
||||||
|
os.chdir(m.work)
|
||||||
|
print("Converting", m.display_frames(count), "skipped", skip_audio, "audio and", skip_video, "video frames")
|
||||||
|
os.system("ffmpeg -f s16le -ac 2 -ar 48000 -i output.raw output.wav")
|
||||||
|
options = "-b:v 4096k -c:v libx264 -preset ultrafast"
|
||||||
|
os.system("ffmpeg -framerate %d -i image-%%06d.jpeg -i output.wav %s output.mp4" % (m.fps, options))
|
||||||
|
os.system("rm output.raw")
|
||||||
|
out_file = m.output+"/video-%04d.mp4" % m.out_index
|
||||||
|
m.out_index += 1
|
||||||
|
os.system("mv output.mp4 "+out_file)
|
||||||
|
print("Converted", m.display_frames(count), "in", "%.1f" % (time.time()-t1), "seconds")
|
||||||
|
if mode == "convert":
|
||||||
|
print("Original size -> new size")
|
||||||
|
os.system("du -sh .; du -sh "+out_file)
|
||||||
|
return count
|
||||||
|
|
||||||
|
def jpegize(m):
|
||||||
|
i = 0
|
||||||
|
os.chdir(m.source)
|
||||||
|
l = os.listdir(m.source)
|
||||||
|
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(i, '/', len(l))
|
||||||
|
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', base+'.jpeg.sv')
|
||||||
|
|
||||||
|
m = Mpegize()
|
||||||
|
m.run(sys.argv)
|
Reference in New Issue
Block a user