summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2025-02-22 19:04:16 -0500
committerpommicket <pommicket@gmail.com>2025-02-25 15:16:06 -0500
commit08c4958f4df36b2c21ae3be4397bf7582a8caff8 (patch)
tree01310210118851f7547d0bad83eeed958acd0b63
parent2a61b4884da2781f3e2705e6b560e59fbaa478ba (diff)
write a video file
-rw-r--r--README.md4
-rw-r--r--main.c160
-rw-r--r--meson.build5
3 files changed, 166 insertions, 3 deletions
diff --git a/README.md b/README.md
index 714795d..11d237a 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,10 @@ It features
# Building from source
camlet requires meson-build, a C compiler, and the development libraries
-for SDL2, SDL2\_ttf, GL (headers only), v4l2, udev, sodium, jpeglib (from IJG), and fontconfig.
+for SDL2, SDL2\_ttf, GL (headers only), v4l2, udev, sodium, jpeglib (from IJG), avcodec, avformat, and fontconfig.
These can all be installed on Debian/Ubuntu with
```sh
-sudo apt install clang meson libv4l-dev libudev-dev libsodium-dev libfontconfig-dev libgl-dev libsdl2-dev libsdl2-ttf-dev libjpeg-dev
+sudo apt install clang meson libv4l-dev libudev-dev libsodium-dev libfontconfig-dev libgl-dev libsdl2-dev libsdl2-ttf-dev libjpeg-dev libavcodec-dev libavformat-dev
```
diff --git a/main.c b/main.c
index 47444fd..4f3a631 100644
--- a/main.c
+++ b/main.c
@@ -20,6 +20,8 @@ TODO
#include <unistd.h>
#include <dirent.h>
#include <pwd.h>
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
#include "ds.h"
#include "camera.h"
@@ -76,6 +78,13 @@ typedef struct {
bool show_debug;
bool menu_needs_rerendering;
bool quit;
+ bool recording_video;
+ AVFormatContext *avf_context;
+ AVCodecContext *video_encoder;
+ AVFrame *video_frame;
+ AVPacket *av_packet;
+ AVStream *video_stream;
+ int64_t video_pts;
int timer;
double timer_activate_time;
double flash_time;
@@ -573,9 +582,160 @@ static bool take_picture(State *state) {
return success;
}
+static bool write_frame(State *state, AVCodecContext *encoder, AVStream *stream, AVFrame *frame) {
+ int err = avcodec_send_frame(encoder, frame);
+ if (err < 0) {
+ fprintf(stderr, "error: avcodec_send_frame: %s\n", av_err2str(err));
+ return false;
+ }
+ while (true) {
+ err = avcodec_receive_packet(encoder, state->av_packet);
+ if (err == AVERROR(EAGAIN) || err == AVERROR_EOF) {
+ break;
+ }
+ if (err < 0) {
+ fprintf(stderr, "error: avcodec_receive_packet: %s\n", av_err2str(err));
+ return false;
+ }
+ state->av_packet->stream_index = stream->index;
+ av_packet_rescale_ts(state->av_packet, encoder->time_base, stream->time_base);
+ err = av_interleaved_write_frame(state->avf_context, state->av_packet);
+ if (err < 0) {
+ fprintf(stderr, "error: av_interleaved_write_frame: %s\n", av_err2str(err));
+ return false;
+ }
+ }
+ return true;
+}
+
+static void stop_video(State *state) {
+ if (state->recording_video) {
+ // flush video encoder
+ write_frame(state, state->video_encoder, state->video_stream, NULL);
+ int err = av_write_trailer(state->avf_context);
+ if (err < 0) {
+ fprintf(stderr, "error: av_write_trailer: %s\n", av_err2str(err));
+ return;
+ }
+ avio_closep(&state->avf_context->pb);
+ }
+ if (state->video_encoder) {
+ avcodec_free_context(&state->video_encoder);
+ }
+ if (state->video_frame) {
+ av_frame_free(&state->video_frame);
+ }
+ if (state->avf_context) {
+ if (state->avf_context->pb) {
+ avio_closep(&state->avf_context->pb);
+ }
+ avformat_free_context(state->avf_context);
+ state->avf_context = NULL;
+ }
+ if (state->av_packet) {
+ av_packet_free(&state->av_packet);
+ }
+}
+
+static bool start_video(State *state, const char *filename) {
+ //if (!state->camera) return false;
+ if (state->recording_video) {
+ return true;
+ }
+ stop_video(state);
+ int err = avformat_alloc_output_context2(&state->avf_context, NULL, NULL, filename);
+ if (!state->avf_context) {
+ fprintf(stderr, "error: avformat_alloc_output_context2: %s\n", av_err2str(err));
+ return false;
+ }
+ const AVOutputFormat *fmt = state->avf_context->oformat;
+ const AVCodec *video_codec = avcodec_find_encoder(fmt->video_codec);
+ if (!video_codec) {
+ fprintf(stderr, "couldn't find encoder for codec %s\n", avcodec_get_name(fmt->video_codec));
+ return false;
+ }
+ state->video_stream = avformat_new_stream(state->avf_context, NULL);
+ state->video_stream->id = 0;
+ state->video_encoder = avcodec_alloc_context3(video_codec);
+ if (!state->video_encoder) {
+ fprintf(stderr, "couldn't create video encoding context\n");
+ return false;
+ }
+ state->av_packet = av_packet_alloc();
+ if (!state->av_packet) {
+ fprintf(stderr, "couldn't allocate video packet\n");
+ return false;
+ }
+ state->video_encoder->codec_id = fmt->video_codec;
+ state->video_encoder->bit_rate = 400000;
+ state->video_encoder->width = 360;//camera_frame_width(state->camera);
+ state->video_encoder->height = 240;//camera_frame_height(state->camera);
+ state->video_encoder->time_base = state->video_stream->time_base = (AVRational){1,30};// TODO: restrict application to 30FPS when recording video
+ state->video_encoder->gop_size = 12;
+ state->video_encoder->pix_fmt = AV_PIX_FMT_YUV420P;
+ if (state->avf_context->oformat->flags & AVFMT_GLOBALHEADER)
+ state->video_encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ err = avcodec_open2(state->video_encoder, video_codec, NULL);
+ if (err < 0) {
+ fprintf(stderr, "error: avcodec_open2: %s\n", av_err2str(err));
+ return false;
+ }
+ err = avcodec_parameters_from_context(state->video_stream->codecpar, state->video_encoder);
+ if (err < 0) {
+ fprintf(stderr, "error: avcodec_parameters_from_context: %s\n", av_err2str(err));
+ return false;
+ }
+ state->video_frame = av_frame_alloc();
+ if (!state->video_frame) {
+ fprintf(stderr, "couldn't allocate video frame\n");
+ return false;
+ }
+ state->video_frame->format = AV_PIX_FMT_YUV420P;
+ state->video_frame->width = state->video_encoder->width;
+ state->video_frame->height = state->video_encoder->height;
+ err = av_frame_get_buffer(state->video_frame, 0);
+ if (err < 0) {
+ fprintf(stderr, "error: av_frame_get_buffer: %s\n", av_err2str(err));
+ return false;
+ }
+ // av_dump_format(state->avf_context, 0, filename, 1);
+ err = avio_open(&state->avf_context->pb, filename, AVIO_FLAG_WRITE);
+ if (err < 0) {
+ fprintf(stderr, "error: avio_open: %s\n", av_err2str(err));
+ return false;
+ }
+ err = avformat_write_header(state->avf_context, NULL);
+ if (err < 0) {
+ fprintf(stderr, "error: avformat_write_header: %s\n", av_err2str(err));
+ return false;
+ }
+ state->recording_video = true;
+ // ----
+ for (int frame = 0; frame < 300; frame++) {
+ err = av_frame_make_writable(state->video_frame);
+ if (err < 0) {
+ fprintf(stderr, "error: av_frame_make_writable: %s\n", av_err2str(err));
+ return false;
+ }
+ for (int y = 0; y < state->video_frame->height; y++) {
+ for (int x = 0; x < state->video_frame->width; x++) {
+ state->video_frame->data[0][y * state->video_frame->linesize[0] + x] = (uint8_t)(x + y+frame);
+ state->video_frame->data[1][(y/2) * state->video_frame->linesize[1] + x/2] = (uint8_t)(x * y);
+ state->video_frame->data[2][(y/2) * state->video_frame->linesize[2] + x/2] = (uint8_t)(x - y);
+ }
+ }
+ state->video_frame->pts = state->video_pts++;
+ write_frame(state, state->video_encoder, state->video_stream, state->video_frame);
+ }
+ return true;
+}
+
int main(void) {
static State state_data;
State *state = &state_data;
+ start_video(state, "out.mkv");
+ stop_video(state);
+ return 0;
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1"); // if this program is sent a SIGTERM/SIGINT, don't turn it into a quit event
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
fprintf(stderr, "couldn't initialize SDL\n");
diff --git a/meson.build b/meson.build
index cbcb06b..3c383c5 100644
--- a/meson.build
+++ b/meson.build
@@ -13,6 +13,9 @@ gl = dependency('GL')
sodium = dependency('libsodium')
fontconfig = dependency('fontconfig')
jpeg = dependency('libjpeg')
+avcodec = dependency('libavcodec')
+avformat = dependency('libavformat')
+avutil = dependency('libavutil')
m_dep = cc.find_library('m', required: false)
if m_dep.found()
add_project_link_arguments('-lm', language: 'c')
@@ -23,5 +26,5 @@ else
debug_def = '-DDEBUG=0'
endif
executable('camlet', 'main.c', 'camera.c', '3rd_party/stb_image_write.c',
- dependencies: [v4l2, sdl2, sdl2_ttf, gl, udev, sodium, fontconfig, jpeg],
+ dependencies: [v4l2, sdl2, sdl2_ttf, gl, udev, sodium, fontconfig, jpeg, avcodec, avformat, avutil],
c_args: ['-Wno-unused-function', '-Wno-format-truncation', '-Wshadow', debug_def])