Hi all,
I saw there is a ticket[1] unfixed since 2014 about adding ffmpeg and/or
libav support to pix_record.
Meanwhile I needed something today, so I wrote a simple PNM plugin (for
8bit PPM (RGB) and PGM (gray) formats), that I can use with named pipes
and encode video with ffmpeg command line tool.
Using this plugin I can record 1024x1024p60 at 60fps with AMD RX 580 GPU
and AMD Ryzen 2700X CPU with pd -batch; probably this is limited by my
monitor refresh rate. Output file in my test case was 200MB for 10mins,
good quality. Compared with my previous method using temporary TIFF
files with pix_write, this new recordPNM with a FIFO (created with
mkfifo) is 10x faster and uses around 0.3% of the disk space.
There may be bugs:
- upside down or not images (my test video was symmetrical enough that I
could not notice)
- pgm (gray) mode is not tested thoroughly
- changing size mid stream might be possible for special purposes (e.g.
mipmap chains) (untested)
- file is opened relative to Pd's current working directory, instead of
relative to the patch containing pix_record
- PNM has (big-endian) 16bit formats, but afaict Gem pixes are only
8bit, so this is for the future
- PNM has a 1bit format too (PBM), this is not supported by this plugin
- PNM has plain text variants of PPM, PGM, and PBM, which are not
supported by this plugin
- reading is not implemented yet (parsing the ASCII header is a bit
subtle with comments possible in the middle of tokens...)
Code attached as git patch against recent git.iem.at master (if you
prefer I could do a github pull request dance for your
entertainment).
[1] https://github.com/umlaeute/Gem/issues/73
Thanks,
Claude
--
https://mathr.co.uk
From 212961efb51aeaaee345fd9c83357e6ef00c0b7f Mon Sep 17 00:00:00 2001
From: Claude Heiland-Allen <[email protected]>
Date: Fri, 14 Oct 2022 12:25:48 +0100
Subject: [PATCH] recordPNM plugin for PPM and PGM image stream output
PNM is a family of lowest common denominator image formats,
with an ASCII header containing metadata, followed by raw
uncompressed image data.
Specification at <https://netpbm.sourceforge.net/doc/pnm.html>.
This plugin writes a single file containing a concatenated
stream of PNM images.
Supports the following codecs:
- ppm (rgb888 PPM P6 binary format)
- pgm (gray8 PGM P5 binary format)
Example use case:
1. `mkfifo fifo.ppm`
2. record frames into `fifo.ppm` with this plugin
3. encode video with `ffmpeg -f image2pipe -i fifo.ppm ...`
Note that the writer may block if data is not read fast enough.
---
configure.ac | 2 +
plugins/Makefile.am | 1 +
plugins/PNM/Makefile.am | 32 +++++
plugins/PNM/recordPNM.cpp | 244 ++++++++++++++++++++++++++++++++++++++
plugins/PNM/recordPNM.h | 110 +++++++++++++++++
5 files changed, 389 insertions(+)
create mode 100644 plugins/PNM/Makefile.am
create mode 100644 plugins/PNM/recordPNM.cpp
create mode 100644 plugins/PNM/recordPNM.h
diff --git a/configure.ac b/configure.ac
index 8530c7f8..11ea6ad5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -39,6 +39,7 @@ AC_CONFIG_FILES([plugins/MPEG1/Makefile])
AC_CONFIG_FILES([plugins/MPEG3/Makefile])
AC_CONFIG_FILES([plugins/NDI/Makefile])
AC_CONFIG_FILES([plugins/OBJ/Makefile])
+AC_CONFIG_FILES([plugins/PNM/Makefile])
AC_CONFIG_FILES([plugins/QT4L/Makefile])
AC_CONFIG_FILES([plugins/QuickTime/Makefile])
AC_CONFIG_FILES([plugins/SGI/Makefile])
@@ -795,6 +796,7 @@ Configuration:
record-support
use DeckLink : ${have_DeckLink}
use NDI : ${have_ndi}
+ use PNM : YES (local)
use QuickTime : ${have_libquicktime}
use v4l : ${have_v4l}
use v4l2 : ${have_v4l2}
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 7babbc96..0181905a 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -21,6 +21,7 @@ SUBDIRS += JPEG
SUBDIRS += MPEG1
SUBDIRS += MPEG3
SUBDIRS += OBJ
+SUBDIRS += PNM
SUBDIRS += QT4L
SUBDIRS += QuickTime
SUBDIRS += SGI
diff --git a/plugins/PNM/Makefile.am b/plugins/PNM/Makefile.am
new file mode 100644
index 00000000..a4ecead0
--- /dev/null
+++ b/plugins/PNM/Makefile.am
@@ -0,0 +1,32 @@
+
+ACLOCAL_AMFLAGS = -I $(top_srcdir)/m4
+AM_CPPFLAGS = -I$(top_srcdir)/src $(GEM_EXTERNAL_CPPFLAGS)
+AM_CXXFLAGS =
+
+pkglib_LTLIBRARIES=
+
+pkglib_LTLIBRARIES+= gem_recordPNM.la
+
+AM_LDFLAGS = -module -avoid-version -shared
+if WINDOWS
+AM_LDFLAGS += -no-undefined
+endif
+gem_recordPNM_la_LIBADD =
+
+# RTE
+AM_CXXFLAGS += $(GEM_RTE_CFLAGS) $(GEM_ARCH_CXXFLAGS)
+AM_LDFLAGS += $(GEM_RTE_LIBS) $(GEM_ARCH_LDFLAGS)
+# flags for building Gem externals
+AM_CXXFLAGS += $(GEM_EXTERNAL_CFLAGS)
+gem_recordPNM_la_LIBADD += -L$(top_builddir) $(GEM_EXTERNAL_LIBS)
+
+# Dependencies
+AM_CXXFLAGS +=
+gem_recordPNM_la_LIBADD +=
+
+# convenience symlinks
+include $(srcdir)/../symlink_ltlib.mk
+
+
+### SOURCES
+gem_recordPNM_la_SOURCES= recordPNM.cpp recordPNM.h
diff --git a/plugins/PNM/recordPNM.cpp b/plugins/PNM/recordPNM.cpp
new file mode 100644
index 00000000..9df6aad8
--- /dev/null
+++ b/plugins/PNM/recordPNM.cpp
@@ -0,0 +1,244 @@
+////////////////////////////////////////////////////////
+//
+// GEM - Graphics Environment for Multimedia
+//
+// [email protected]
+//
+// Implementation file
+//
+// Copyright (c) 1997-1999 Mark Danks.
+// Copyright (c) Günther Geiger.
+// Copyright (c) 2001-2011 IOhannes m zmölnig. forum::für::umläute. IEM. [email protected]
+// Copyright (c) 2022 Claude Heiland-Allen. [email protected]
+// For information on usage and redistribution, and for a DISCLAIMER OF ALL
+// WARRANTIES, see the file, "GEM.LICENSE.TERMS" in this distribution.
+//
+// Future plans
+//
+// - support 16bit PNM formats (PPM, PGM)
+// - support 1bit PNM format (PBM)
+// - support plain (ascii decimal) formats
+//
+/////////////////////////////////////////////////////////
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "recordPNM.h"
+#include "Gem/RTE.h"
+#include "Gem/Manager.h"
+
+#include "plugins/PluginFactory.h"
+
+
+using namespace gem::plugins;
+
+#include <stdio.h>
+
+#ifdef GEM_USE_RECORDPNM
+REGISTER_RECORDFACTORY("PNM", recordPNM);
+#endif
+
+/////////////////////////////////////////////////////////
+//
+// recordPNM
+//
+/////////////////////////////////////////////////////////
+// Constructor
+//
+/////////////////////////////////////////////////////////
+recordPNM :: recordPNM(void)
+#if defined GEM_USE_RECORDPNM
+ : m_file(NULL)
+ , m_channels(3)
+ , m_image()
+{
+}
+#else
+{}
+#endif
+
+////////////////////////////////////////////////////////
+// Destructor
+//
+/////////////////////////////////////////////////////////
+recordPNM :: ~recordPNM(void)
+{
+ stop();
+}
+
+#if defined GEM_USE_RECORDPNM
+void recordPNM :: stop(void)
+{
+ if(m_file) {
+ fclose(m_file);
+ m_file = nullptr;
+ }
+}
+
+/////////////////////////////////////////////////////////
+// open a file !
+//
+/////////////////////////////////////////////////////////
+/* guess the file-format by inspecting the extension */
+static int guess_channels(const std::string&filename)
+{
+ const char * extension = strrchr(filename.c_str(), '.');
+ unsigned int i=0;
+
+ if(!extension) {
+ verbose(0,
+ "[GEM:recordPNM] no extension given: encoding will be PPM");
+ return 3;
+ }
+
+ extension++;
+
+ if(!strcasecmp(extension, "pgm")) {
+ return 1;
+ }
+ if(!strcasecmp(extension, "ppm")) {
+ return 3;
+ }
+
+ verbose(0,
+ "[GEM:recordPNM] unknown extension: encoding will be PPM");
+ return 3;
+}
+
+
+bool recordPNM :: start(const std::string&filename, gem::Properties&props)
+{
+ stop();
+
+ m_channels = guess_channels(filename);
+
+ m_file = fopen(filename.c_str(), "wb");
+ if(m_file==NULL) {
+ pd_error(0, "[GEM:recordPNM] starting to record to %s failed",
+ filename.c_str());
+ return false;
+ }
+
+ return (true);
+}
+
+
+/////////////////////////////////////////////////////////
+// initialize the encoder
+//
+/////////////////////////////////////////////////////////
+bool recordPNM :: init(const imageStruct*img, double fps)
+{
+ if(!m_file || !img || fps < 0.) {
+ return false;
+ }
+ return true;
+}
+
+/////////////////////////////////////////////////////////
+// do the actual encoding and writing to file
+//
+/////////////////////////////////////////////////////////
+bool recordPNM :: write(imageStruct*img)
+{
+ if(!m_file || !img) {
+ return false;
+ }
+
+ switch(m_channels) {
+ case 1:
+ m_image.convertFrom(img, GEM_GRAY);
+ break;
+ case 3:
+ m_image.convertFrom(img, GEM_RGB);
+ break;
+ default:
+ pd_error(0, "[GEM:recordPNM] unsupported channels %d...", m_channels);
+ return false;
+ }
+
+ if (m_image.csize != m_channels) {
+ pd_error(0, "[GEM:recordPNM] unsupported data layout %d != %d...",
+ m_image.csize, m_channels);
+ return false;
+ }
+
+ if(fprintf(m_file, "P%d\n%d %d\n255\n",
+ m_channels == 1 ? 5 : 6, m_image.xsize, m_image.ysize) < 0) {
+ pd_error(0, "[GEM:recordPNM] error writing image header...");
+ return false;
+ }
+
+ size_t stride = m_image.xsize * m_image.csize;
+ if (! m_image.upsidedown) {
+ if(fwrite(m_image.data, m_image.ysize * stride, 1, m_file) != 1) {
+ pd_error(0, "[GEM:recordPNM] error writing image data...");
+ return false;
+ }
+ }
+ else {
+ for (int y = m_image.ysize - 1; y >= 0; --y) {
+ if(fwrite(m_image.data + y * stride, stride, 1, m_file) != 1) {
+ pd_error(0, "[GEM:recordPNM] error writing image data row...");
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+
+/////////////////////////////////////////////////////////
+// codecs
+//
+/////////////////////////////////////////////////////////
+
+bool recordPNM :: setCodec(const std::string &name)
+{
+ if(name == "ppm") {
+ m_channels = 3;
+ return true;
+ } else if(name == "pgm") {
+ m_channels = 1;
+ return true;
+ } else {
+ pd_error(0, "[GEM:recordPNM] unknown codec...");
+ return false;
+ }
+}
+
+std::vector<std::string> recordPNM :: getCodecs(void)
+{
+ std::vector<std::string> codecs;
+ codecs.push_back("ppm");
+ codecs.push_back("pgm");
+ return codecs;
+}
+
+const std::string recordPNM :: getCodecDescription(const std::string&codecname)
+{
+ if(codecname == "ppm") {
+ return "Portable Pixmap (PPM)";
+ }
+ else if(codecname == "pgm") {
+ return "Portable Greymap (PGM)";
+ }
+ else {
+ pd_error(0, "[GEM:recordPNM] cannot describe unknown codec...");
+ return "(unknown codec)";
+ }
+}
+
+/////////////////////////////////////////////////////////
+// properties
+//
+/////////////////////////////////////////////////////////
+
+bool recordPNM :: enumProperties(gem::Properties&props)
+{
+ props.clear();
+ return true;
+}
+
+#endif
diff --git a/plugins/PNM/recordPNM.h b/plugins/PNM/recordPNM.h
new file mode 100644
index 00000000..59215ef7
--- /dev/null
+++ b/plugins/PNM/recordPNM.h
@@ -0,0 +1,110 @@
+/* -----------------------------------------------------------------
+
+GEM - Graphics Environment for Multimedia
+
+Record a sequence of images to a single-file PNM stream.
+
+Copyright (c) 1997-1999 Mark Danks. [email protected]
+Copyright (c) Günther Geiger. [email protected]
+Copyright (c) 2001-2011 IOhannes m zmölnig. forum::für::umläute. IEM. [email protected]
+Copyright (c) 2022 Claude Heiland-Allen. [email protected]
+For information on usage and redistribution, and for a DISCLAIMER OF ALL
+WARRANTIES, see the file, "GEM.LICENSE.TERMS" in this distribution.
+
+-----------------------------------------------------------------*/
+
+#ifndef _INCLUDE_GEMPLUGIN__RECORDPNM_RECORDFFMPEG_H_
+#define _INCLUDE_GEMPLUGIN__RECORDPNM_RECORDFFMPEG_H_
+
+#include "plugins/record.h"
+
+#define GEM_USE_RECORDPNM
+
+#include <map>
+
+/*---------------------------------------------------------------
+ -------------------------------------------------------------------
+ CLASS
+ recordPNM
+
+ class for recording video-streams in PNM format (PPM, PGM)
+
+ KEYWORDS
+ pix record movie
+
+ DESCRIPTION
+
+ -----------------------------------------------------------------*/
+namespace gem
+{
+namespace plugins
+{
+class GEM_EXPORT recordPNM : public record
+{
+public:
+
+ //////////
+ // Constructor
+ recordPNM(void);
+
+ ////////
+ // Destructor
+ virtual ~recordPNM(void);
+
+#if defined GEM_USE_RECORDPNM
+
+ //////////
+ // close the movie file
+ // stop recording, close the file and clean up temporary things
+ virtual void stop(void);
+
+ //////////
+ // open a movie up
+ // open the recordPNM "filename" (think better about URIs ?)
+ // returns TRUE if opening was successfull, FALSE otherwise
+ virtual bool start(const std::string&filename, gem::Properties&props);
+
+ //////////
+ // initialize the encoder
+ // dummyImage provides meta-information (e.g. size) that must not change during the encoding cycle
+ // fps is the number of frames per second
+ //
+ // returns TRUE if init was successfull, FALSE otherwise
+ virtual bool init(const imageStruct* dummyImage, const double fps);
+
+ //////////
+ // write the next frame
+ virtual bool write(imageStruct*);
+
+ //////////
+ // codecs
+ virtual bool setCodec(const std::string&name);
+ virtual std::vector<std::string>getCodecs(void);
+ virtual const std::string getCodecDescription(const std::string&codecname);
+
+ //////////
+ // properties
+ virtual bool enumProperties(gem::Properties&props);
+
+ virtual bool dialog(void)
+ {
+ return false;
+ }
+
+private:
+
+ /* output stream */
+ FILE *m_file;
+
+ /* 3 (rgb) or 1 (gray) */
+ int m_channels;
+
+ /* converted image */
+ imageStruct m_image;
+
+#endif /* PNM */
+};
+};
+};
+
+#endif // for header file
--
2.35.1
_______________________________________________
GEM-dev mailing list
[email protected]
https://lists.puredata.info/listinfo/gem-dev