/* GStreamer mpeg2enc (mjpegtools) wrapper * (c) 2003 Ronald Bultje * (c) 2006 Mark Nauwelaerts * * gstmpeg2enc.cc: gstreamer mpeg2enc wrapping * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301, USA. */ /** * SECTION:element-mpeg2enc * @see_also: mpeg2dec * * This element encodes raw video into an MPEG-1/2 elementary stream using the * [mjpegtools](http://mjpeg.sourceforge.net/) library. * * Documentation on MPEG encoding in general can be found in the * [MJPEG Howto](https://sourceforge.net/docman/display_doc.php?docid=3456&group_id=5776) * and on the various available parameters in the documentation * of the mpeg2enc tool in particular, which shares options with this element. * * ## Example pipeline * * |[ * gst-launch-1.0 videotestsrc num-buffers=1000 ! mpeg2enc ! filesink location=videotestsrc.m1v * ]| This example pipeline will encode a test video source to a an MPEG1 * elementary stream (with Generic MPEG1 profile). * * Likely, the #GstMpeg2enc:format property * is most important, as it selects the type of MPEG stream that is produced. * In particular, default property values are dependent on the format, * and can even be forcibly restrained to certain pre-sets (and thereby ignored). * Note that the (S)VCD profiles also restrict the image size, so some scaling * may be needed to accommodate this. The so-called generic profiles (as used * in the example above) allow most parameters to be adjusted. * * |[ * gst-launch-1.0 videotestsrc num-buffers=1000 ! videoscale ! mpeg2enc format=1 norm=p ! filesink location=videotestsrc.m1v * ]| This will produce an MPEG1 profile stream according to VCD2.0 specifications * for PAL #GstMpeg2enc:norm (as the image height is dependent on video norm). */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include "gstmpeg2enc.hh" GST_DEBUG_CATEGORY (mpeg2enc_debug); #define COMMON_VIDEO_CAPS \ "width = (int) [ 16, 4096 ], " \ "height = (int) [ 16, 4096 ], " \ "framerate = " \ " (fraction) { 24000/1001, 24/1, 25/1, 30000/1001, 30/1, 50/1, 60000/1001 }" static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("video/x-raw, format = (string) I420, " COMMON_VIDEO_CAPS) ); static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("video/mpeg, " "systemstream = (boolean) false, " "mpegversion = (int) { 1, 2 }, " COMMON_VIDEO_CAPS) ); static void gst_mpeg2enc_finalize (GObject * object); static void gst_mpeg2enc_reset (GstMpeg2enc * enc); static gboolean gst_mpeg2enc_setcaps (GstMpeg2enc * enc, GstPad * pad, GstCaps * caps); static gboolean gst_mpeg2enc_sink_query (GstPad * pad, GstObject * parent, GstQuery * query); static gboolean gst_mpeg2enc_sink_event (GstPad * pad, GstObject * parent, GstEvent * event); static void gst_mpeg2enc_loop (GstMpeg2enc * enc); static GstFlowReturn gst_mpeg2enc_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer); static gboolean gst_mpeg2enc_src_activate_mode (GstPad * pad, GstObject * parent, GstPadMode mode, gboolean active); static GstStateChangeReturn gst_mpeg2enc_change_state (GstElement * element, GstStateChange transition); static void gst_mpeg2enc_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static void gst_mpeg2enc_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); #define gst_mpeg2enc_parent_class parent_class G_DEFINE_TYPE_WITH_CODE (GstMpeg2enc, gst_mpeg2enc, GST_TYPE_ELEMENT, G_IMPLEMENT_INTERFACE (GST_TYPE_PRESET, NULL)); static void gst_mpeg2enc_class_init (GstMpeg2encClass * klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GstElementClass *element_class = GST_ELEMENT_CLASS (klass); GST_DEBUG_CATEGORY_INIT (mpeg2enc_debug, "mpeg2enc", 0, "MPEG1/2 encoder"); object_class->set_property = gst_mpeg2enc_set_property; object_class->get_property = gst_mpeg2enc_get_property; /* register properties */ GstMpeg2EncOptions::initProperties (object_class); object_class->finalize = GST_DEBUG_FUNCPTR (gst_mpeg2enc_finalize); element_class->change_state = GST_DEBUG_FUNCPTR (gst_mpeg2enc_change_state); gst_element_class_add_static_pad_template (element_class, &src_template); gst_element_class_add_static_pad_template (element_class, &sink_template); gst_element_class_set_static_metadata (element_class, "mpeg2enc video encoder", "Codec/Encoder/Video", "High-quality MPEG-1/2 video encoder", "Andrew Stevens \n" "Ronald Bultje "); } static void gst_mpeg2enc_finalize (GObject * object) { GstMpeg2enc *enc = GST_MPEG2ENC (object); if (enc->encoder) { delete enc->encoder; enc->encoder = NULL; } delete enc->options; g_queue_free (enc->time); g_mutex_clear (&enc->tlock); g_cond_clear (&enc->cond); G_OBJECT_CLASS (parent_class)->finalize (object); } static void gst_mpeg2enc_init (GstMpeg2enc * enc) { GstElement *element = GST_ELEMENT (enc); enc->sinkpad = gst_pad_new_from_static_template (&sink_template, "sink"); gst_pad_set_query_function (enc->sinkpad, GST_DEBUG_FUNCPTR (gst_mpeg2enc_sink_query)); gst_pad_set_event_function (enc->sinkpad, GST_DEBUG_FUNCPTR (gst_mpeg2enc_sink_event)); gst_pad_set_chain_function (enc->sinkpad, GST_DEBUG_FUNCPTR (gst_mpeg2enc_chain)); gst_element_add_pad (element, enc->sinkpad); enc->srcpad = gst_pad_new_from_static_template (&src_template, "src"); gst_pad_use_fixed_caps (enc->srcpad); gst_pad_set_activatemode_function (enc->srcpad, GST_DEBUG_FUNCPTR (gst_mpeg2enc_src_activate_mode)); gst_element_add_pad (element, enc->srcpad); enc->options = new GstMpeg2EncOptions (); enc->encoder = NULL; enc->buffer = NULL; g_mutex_init (&enc->tlock); g_cond_init (&enc->cond); enc->time = g_queue_new (); gst_mpeg2enc_reset (enc); } static void gst_mpeg2enc_reset (GstMpeg2enc * enc) { GstBuffer *buf; enc->eos = FALSE; enc->srcresult = GST_FLOW_OK; /* in case of error'ed ending */ if (enc->buffer) gst_buffer_unref (enc->buffer); enc->buffer = NULL; while ((buf = (GstBuffer *) g_queue_pop_head (enc->time))) gst_buffer_unref (buf); if (enc->encoder) { delete enc->encoder; enc->encoder = NULL; } } /* some (!) coding to get caps depending on the video norm and chosen format */ static void gst_mpeg2enc_add_fps (GstStructure * structure, gint fpss[]) { GValue list = { 0, }, fps = { 0,}; guint n; g_value_init (&list, GST_TYPE_LIST); g_value_init (&fps, GST_TYPE_FRACTION); for (n = 0; fpss[n] != 0; n += 2) { gst_value_set_fraction (&fps, fpss[n], fpss[n + 1]); gst_value_list_append_value (&list, &fps); } gst_structure_set_value (structure, "framerate", &list); g_value_unset (&list); g_value_unset (&fps); } static inline gint * gst_mpeg2enc_get_fps (GstMpeg2enc * enc) { static gint fps_pal[] = { 24, 1, 25, 1, 50, 1, 0 }; static gint fps_ntsc[] = { 24000, 1001, 24, 1, 30000, 1001, 30, 1, 60000, 1001, 0 }; static gint fps_all[] = { 24000, 1001, 24, 1, 30000, 1001, 30, 1, 60000, 1001, 25, 1, 50, 1, 0 }; if (enc->options->norm == 'n') return fps_ntsc; else if (enc->options->norm == 0) return fps_all; else return fps_pal; } static GstStructure * gst_mpeg2enc_structure_from_norm (GstMpeg2enc * enc, gint horiz, gint pal_v, gint ntsc_v) { GstStructure *structure; structure = gst_structure_new ("video/x-raw", "format", G_TYPE_STRING, "I420", NULL); switch (enc->options->norm) { case 0: { GValue list = { 0, } , val = { 0,}; g_value_init (&list, GST_TYPE_LIST); g_value_init (&val, G_TYPE_INT); g_value_set_int (&val, pal_v); gst_value_list_append_value (&list, &val); g_value_set_int (&val, ntsc_v); gst_value_list_append_value (&list, &val); gst_structure_set_value (structure, "height", &list); g_value_unset (&list); g_value_unset (&val); break; } case 'n': gst_structure_set (structure, "height", G_TYPE_INT, ntsc_v, (void *) NULL); break; default: gst_structure_set (structure, "height", G_TYPE_INT, pal_v, (void *) NULL); break; } gst_structure_set (structure, "width", G_TYPE_INT, horiz, (void *) NULL); gst_mpeg2enc_add_fps (structure, gst_mpeg2enc_get_fps (enc)); return structure; } static GstCaps * gst_mpeg2enc_getcaps (GstMpeg2enc * enc, GstPad * pad) { GstCaps *caps; caps = gst_pad_get_current_caps (pad); if (caps) { gst_caps_ref (caps); return caps; } switch (enc->options->format) { case 1: /* vcd */ case 2: /* user vcd */ caps = gst_caps_new_full (gst_mpeg2enc_structure_from_norm (enc, 352, 288, 240), NULL); break; case 4: /* svcd */ case 5: /* user svcd */ caps = gst_caps_new_full (gst_mpeg2enc_structure_from_norm (enc, 480, 576, 480), NULL); break; case 6: /* vcd stills */ /* low resolution */ caps = gst_caps_new_full (gst_mpeg2enc_structure_from_norm (enc, 352, 288, 240), NULL); /* high resolution */ gst_caps_append_structure (caps, gst_mpeg2enc_structure_from_norm (enc, 704, 576, 480)); break; case 7: /* svcd stills */ /* low resolution */ caps = gst_caps_new_full (gst_mpeg2enc_structure_from_norm (enc, 480, 576, 480), NULL); /* high resolution */ gst_caps_append_structure (caps, gst_mpeg2enc_structure_from_norm (enc, 704, 576, 480)); break; case 0: case 3: case 8: case 9: default: caps = gst_caps_copy (gst_pad_get_pad_template_caps (pad)); gst_mpeg2enc_add_fps (gst_caps_get_structure (caps, 0), gst_mpeg2enc_get_fps (enc)); break; } GST_DEBUG_OBJECT (enc, "returned caps %" GST_PTR_FORMAT, caps); return caps; } static gboolean gst_mpeg2enc_sink_query (GstPad * pad, GstObject * parent, GstQuery * query) { GstMpeg2enc *enc; gboolean res = FALSE; enc = GST_MPEG2ENC (parent); switch (GST_QUERY_TYPE (query)) { case GST_QUERY_CAPS: { GstCaps *filter, *caps; gst_query_parse_caps (query, &filter); caps = gst_mpeg2enc_getcaps (enc, pad); gst_query_set_caps_result (query, caps); gst_caps_unref (caps); res = TRUE; } break; default: res = gst_pad_query_default (pad, parent, query); break; } return res; } static gboolean gst_mpeg2enc_setcaps (GstMpeg2enc * enc, GstPad * pad, GstCaps * caps) { GstCaps *othercaps = NULL; gboolean ret; /* does not go well to restart stream mid-way */ if (enc->encoder) goto refuse_renegotiation; /* since mpeg encoder does not really check, let's check caps */ if (!gst_video_info_from_caps (&enc->vinfo, caps)) goto refuse_caps; if (GST_VIDEO_INFO_FORMAT (&enc->vinfo) != GST_VIDEO_FORMAT_I420) goto refuse_caps; /* create new encoder with these settings */ enc->encoder = new GstMpeg2Encoder (enc->options, GST_ELEMENT (enc), caps); if (!enc->encoder->setup ()) goto refuse_caps; /* and set caps on other side, which should accept anyway */ othercaps = enc->encoder->getFormat (); ret = gst_pad_set_caps (enc->srcpad, othercaps); gst_caps_unref (othercaps); othercaps = NULL; if (!ret) goto refuse_caps; /* now that we have all the setup and buffers are expected incoming; * task can get going */ gst_pad_start_task (enc->srcpad, (GstTaskFunction) gst_mpeg2enc_loop, enc, NULL); return TRUE; refuse_caps: { GST_WARNING_OBJECT (enc, "refused caps %" GST_PTR_FORMAT, caps); if (othercaps) gst_caps_unref (othercaps); if (enc->encoder) { delete enc->encoder; enc->encoder = NULL; } return FALSE; } refuse_renegotiation: { GST_WARNING_OBJECT (enc, "refused renegotiation (to %" GST_PTR_FORMAT ")", caps); return FALSE; } } static gboolean gst_mpeg2enc_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) { GstMpeg2enc *enc; gboolean result = TRUE; enc = GST_MPEG2ENC (parent); switch (GST_EVENT_TYPE (event)) { case GST_EVENT_FLUSH_START: /* forward event */ result = gst_pad_push_event (enc->srcpad, event); /* no special action as there is not much to flush; * neither is it possible to halt the mpeg encoding loop */ goto done; case GST_EVENT_FLUSH_STOP: /* forward event */ result = gst_pad_push_event (enc->srcpad, event); if (!result) goto done; /* this clears the error state in case of a failure in encoding task; * so chain function can carry on again */ GST_MPEG2ENC_MUTEX_LOCK (enc); enc->srcresult = GST_FLOW_OK; GST_MPEG2ENC_MUTEX_UNLOCK (enc); goto done; case GST_EVENT_EOS: /* inform the encoding task that it can stop now */ GST_MPEG2ENC_MUTEX_LOCK (enc); enc->eos = TRUE; GST_MPEG2ENC_SIGNAL (enc); GST_MPEG2ENC_MUTEX_UNLOCK (enc); /* eat this event for now, task will send eos when finished */ gst_event_unref (event); goto done; case GST_EVENT_CAPS: { GstCaps *caps; gst_event_parse_caps (event, &caps); result = gst_mpeg2enc_setcaps (enc, pad, caps); gst_event_unref (event); goto done; } default: /* for a serialized event, wait until an earlier buffer is gone, * though this is no guarantee as to when the encoder is done with it */ if (GST_EVENT_IS_SERIALIZED (event)) { GST_MPEG2ENC_MUTEX_LOCK (enc); while (enc->buffer) GST_MPEG2ENC_WAIT (enc); GST_MPEG2ENC_MUTEX_UNLOCK (enc); } break; } result = gst_pad_push_event (enc->srcpad, event); done: return result; } static void gst_mpeg2enc_loop (GstMpeg2enc * enc) { /* do not try to resume or start when output problems; * also ensures a proper (forced) state change */ if (enc->srcresult != GST_FLOW_OK) goto ignore; if (enc->encoder) { /* note that init performs a pre-fill and therefore needs buffers */ enc->encoder->init (); /* task will stay in here during all of the encoding */ enc->encoder->encode (); /* if not well and truly eos, something strange happened */ if (!enc->eos) { GST_ERROR_OBJECT (enc, "encoding task ended without being eos"); /* notify the chain function that it's over */ GST_MPEG2ENC_MUTEX_LOCK (enc); enc->srcresult = GST_FLOW_ERROR; GST_MPEG2ENC_SIGNAL (enc); GST_MPEG2ENC_MUTEX_UNLOCK (enc); } else { /* send eos if this was not a forced stop or other problem */ if (enc->srcresult == GST_FLOW_OK) gst_pad_push_event (enc->srcpad, gst_event_new_eos ()); goto eos; } } else { GST_WARNING_OBJECT (enc, "task started without Mpeg2Encoder"); } /* fall-through */ done: { /* no need to run wildly, stopped elsewhere, e.g. state change */ GST_DEBUG_OBJECT (enc, "pausing encoding task"); gst_pad_pause_task (enc->srcpad); return; } eos: { GST_DEBUG_OBJECT (enc, "encoding task reached eos"); goto done; } ignore: { GST_DEBUG_OBJECT (enc, "not looping because encoding task encountered %s", gst_flow_get_name (enc->srcresult)); goto done; } } static GstFlowReturn gst_mpeg2enc_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) { GstMpeg2enc *enc; enc = GST_MPEG2ENC (parent); if (G_UNLIKELY (!enc->encoder)) goto not_negotiated; GST_MPEG2ENC_MUTEX_LOCK (enc); if (G_UNLIKELY (enc->eos)) goto eos; if (G_UNLIKELY (enc->srcresult != GST_FLOW_OK)) goto ignore; /* things look good, now inform the encoding task that a buffer is ready */ while (enc->buffer) GST_MPEG2ENC_WAIT (enc); enc->buffer = buffer; g_queue_push_tail (enc->time, gst_buffer_ref (buffer)); GST_MPEG2ENC_SIGNAL (enc); GST_MPEG2ENC_MUTEX_UNLOCK (enc); /* buffer will be released by task */ return GST_FLOW_OK; /* special cases */ not_negotiated: { GST_ELEMENT_ERROR (enc, CORE, NEGOTIATION, (NULL), ("format wasn't negotiated before chain function")); gst_buffer_unref (buffer); return GST_FLOW_NOT_NEGOTIATED; } eos: { GST_DEBUG_OBJECT (enc, "ignoring buffer at end-of-stream"); GST_MPEG2ENC_MUTEX_UNLOCK (enc); gst_buffer_unref (buffer); return GST_FLOW_EOS; } ignore: { GstFlowReturn ret = enc->srcresult; GST_DEBUG_OBJECT (enc, "ignoring buffer because encoding task encountered %s", gst_flow_get_name (enc->srcresult)); GST_MPEG2ENC_MUTEX_UNLOCK (enc); gst_buffer_unref (buffer); return ret; } } static void gst_mpeg2enc_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GST_MPEG2ENC (object)->options->getProperty (prop_id, value); } static void gst_mpeg2enc_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GST_MPEG2ENC (object)->options->setProperty (prop_id, value); } static gboolean gst_mpeg2enc_src_activate_mode (GstPad * pad, GstObject * parent, GstPadMode mode, gboolean active) { gboolean result = TRUE; GstMpeg2enc *enc; enc = GST_MPEG2ENC (parent); if (mode != GST_PAD_MODE_PUSH) return FALSE; if (active) { /* setcaps will start task once encoder is setup */ } else { /* can only end the encoding loop by forcing eos */ GST_MPEG2ENC_MUTEX_LOCK (enc); enc->eos = TRUE; enc->srcresult = GST_FLOW_FLUSHING; GST_MPEG2ENC_SIGNAL (enc); GST_MPEG2ENC_MUTEX_UNLOCK (enc); /* encoding loop should have ended now and can be joined */ result = gst_pad_stop_task (pad); } return result; } static GstStateChangeReturn gst_mpeg2enc_change_state (GstElement * element, GstStateChange transition) { GstMpeg2enc *enc = GST_MPEG2ENC (element); GstStateChangeReturn ret; ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); if (ret == GST_STATE_CHANGE_FAILURE) goto done; switch (transition) { case GST_STATE_CHANGE_PAUSED_TO_READY: gst_mpeg2enc_reset (enc); break; default: break; } done: return ret; } #ifndef GST_DISABLE_GST_DEBUG static mjpeg_log_handler_t old_handler = NULL; /* note that this will affect all mjpegtools elements/threads */ static void gst_mpeg2enc_log_callback (log_level_t level, const char *message) { GstDebugLevel gst_level; #if GST_MJPEGTOOLS_API >= 10903 static const gint mjpeg_log_error = mjpeg_loglev_t ("error"); static const gint mjpeg_log_warn = mjpeg_loglev_t ("warn"); static const gint mjpeg_log_info = mjpeg_loglev_t ("info"); static const gint mjpeg_log_debug = mjpeg_loglev_t ("debug"); #else static const gint mjpeg_log_error = LOG_ERROR; static const gint mjpeg_log_warn = LOG_WARN; static const gint mjpeg_log_info = LOG_INFO; static const gint mjpeg_log_debug = LOG_DEBUG; #endif if (level == mjpeg_log_error) { gst_level = GST_LEVEL_ERROR; } else if (level == mjpeg_log_warn) { gst_level = GST_LEVEL_WARNING; } else if (level == mjpeg_log_info) { gst_level = GST_LEVEL_INFO; } else if (level == mjpeg_log_debug) { gst_level = GST_LEVEL_DEBUG; } else { gst_level = GST_LEVEL_INFO; } /* message could have a % in it, do not segfault in such case */ gst_debug_log (mpeg2enc_debug, gst_level, "", "", 0, NULL, "%s", message); /* chain up to the old handler; * this could actually be a handler from another mjpegtools based * plugin; in which case messages can come out double or from * the wrong plugin (element)... */ old_handler (level, message); } #endif static gboolean plugin_init (GstPlugin * plugin) { #ifndef GST_DISABLE_GST_DEBUG old_handler = mjpeg_log_set_handler (gst_mpeg2enc_log_callback); g_assert (old_handler != NULL); #endif /* in any case, we do not want default handler output */ mjpeg_default_handler_verbosity (0); return gst_element_register (plugin, "mpeg2enc", GST_RANK_MARGINAL, GST_TYPE_MPEG2ENC); } GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, mpeg2enc, "High-quality MPEG-1/2 video encoder", plugin_init, VERSION, "GPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)