/* GStreamer * Copyright (C) 2010 David Schleef * Copyright (C) 2025 Igalia, S.L. * author Victor Jaquez * * 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-y4mdec * @title: gsty4mdec * * The gsty4mdec element decodes uncompressed video in YUV4MPEG format. * * ## Example launch line * |[ * gst-launch-1.0 -v filesrc location=file.y4m ! y4mdec ! xvimagesink * ]| * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include "gsty4mdec.h" #include "gsty4mformat.h" #define MAX_SIZE 32768 #define MAX_STREAM_HEADER_LENGTH 128 #define Y4M_STREAM_MAGIC "YUV4MPEG2" #define Y4M_STREAM_MAGIC_LEN 9 #define Y4M_FRAME_MAGIC "FRAME" #define Y4M_FRAME_MAGIC_LEN 5 GST_DEBUG_CATEGORY (y4mdec_debug); #define GST_CAT_DEFAULT y4mdec_debug /* prototypes */ static gboolean gst_y4m_dec_stop (GstBaseParse * parse); static gboolean gst_y4m_dec_start (GstBaseParse * parse); static GstFlowReturn gst_y4m_dec_handle_frame (GstBaseParse * parse, GstBaseParseFrame * frame, gint * skipsize); static gboolean gst_y4m_dec_sink_query (GstBaseParse * parse, GstQuery * query); static gboolean gst_y4m_dec_sink_event (GstBaseParse * parse, GstEvent * event); static gboolean gst_y4m_dec_src_event (GstBaseParse * parse, GstEvent * event); /* pad templates */ static GstStaticPadTemplate gst_y4m_dec_sink_template = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("application/x-yuv4mpeg, y4mversion=2") ); static GstStaticPadTemplate gst_y4m_dec_src_template = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE (Y4M_VIDEO_FORMATS))); /* class initialization */ #define gst_y4m_dec_parent_class parent_class G_DEFINE_TYPE (GstY4mDec, gst_y4m_dec, GST_TYPE_BASE_PARSE); GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (y4mdec, "y4mdec", GST_RANK_SECONDARY, gst_y4m_dec_get_type (), GST_DEBUG_CATEGORY_INIT (y4mdec_debug, "y4mdec", 0, "y4mdec element")); enum ParserState { PARSER_STATE_NONE = 1 << 0, PARSER_STATE_GOT_HEADER = 1 << 1, PARSER_STATE_GOT_FRAME = 2 << 1, }; static void gst_y4m_dec_class_init (GstY4mDecClass * klass) { GstElementClass *element_class = GST_ELEMENT_CLASS (klass); GstBaseParseClass *parse_class = GST_BASE_PARSE_CLASS (klass); parse_class->stop = GST_DEBUG_FUNCPTR (gst_y4m_dec_stop); parse_class->start = GST_DEBUG_FUNCPTR (gst_y4m_dec_start); parse_class->handle_frame = GST_DEBUG_FUNCPTR (gst_y4m_dec_handle_frame); parse_class->sink_query = GST_DEBUG_FUNCPTR (gst_y4m_dec_sink_query); parse_class->sink_event = GST_DEBUG_FUNCPTR (gst_y4m_dec_sink_event); parse_class->src_event = GST_DEBUG_FUNCPTR (gst_y4m_dec_src_event); gst_element_class_add_static_pad_template (element_class, &gst_y4m_dec_src_template); gst_element_class_add_static_pad_template (element_class, &gst_y4m_dec_sink_template); gst_element_class_set_static_metadata (element_class, "YUV4MPEG demuxer/decoder", "Codec/Demuxer", "Demuxes/decodes YUV4MPEG streams", "David Schleef \n" "Victor Jaquez "); } static void gst_y4m_dec_init (GstY4mDec * y4mdec) { } static gboolean gst_y4m_dec_reset (GstBaseParse * parse) { GstY4mDec *y4mdec = GST_Y4M_DEC (parse); GST_TRACE_OBJECT (y4mdec, "start"); y4mdec->state = PARSER_STATE_NONE; gst_base_parse_set_min_frame_size (parse, MAX_STREAM_HEADER_LENGTH); return TRUE; } static gboolean gst_y4m_dec_start (GstBaseParse * parse) { return gst_y4m_dec_reset (parse); } static gboolean gst_y4m_dec_stop (GstBaseParse * parse) { GstY4mDec *y4mdec = GST_Y4M_DEC (parse); if (y4mdec->pool) { gst_buffer_pool_set_active (y4mdec->pool, FALSE); gst_clear_object (&y4mdec->pool); } return TRUE; } static gboolean parse_ratio (const char *param, gulong * n, gulong * d) { char *end; *n = g_ascii_strtoull (param, &end, 10); if (end == param) return FALSE; param = end; if (param[0] != ':') return FALSE; param++; *d = g_ascii_strtoull (param, &end, 10); if (end == param) return FALSE; return TRUE; } static gboolean gst_y4m_dec_parse_header (GstY4mDec * y4mdec, const char *header) { guint len; char **params; guint interlaced_char = 0; gulong fps_n = 0, fps_d = 0; gulong par_n = 0, par_d = 0; gulong width = 0, height = 0; GstVideoFormat format = GST_VIDEO_FORMAT_I420; GstVideoChromaSite chroma_site = GST_VIDEO_CHROMA_SITE_NONE; GstVideoInterlaceMode interlace_mode; if (memcmp (header, "YUV4MPEG2 ", 10) != 0) { GST_ERROR_OBJECT (y4mdec, "y4m start code not found"); return FALSE; } header += 10; if (!g_str_is_ascii (header)) { GST_ERROR_OBJECT (y4mdec, "Invalid non-ASCII y4m header: %s", header); return FALSE; } GST_INFO_OBJECT (y4mdec, "Found header: %s", header); params = g_strsplit (header, " ", -1); len = g_strv_length (params); for (int i = 0; i < len; i++) { const char *param = params[i]; char param_type = *param; const char *param_value = param + 1; switch (param_type) { case 'C': if (!gst_y4m_video_get_format_from_chroma_tag (param_value, &format, &chroma_site)) { GST_ERROR_OBJECT (y4mdec, "Failed to parse colorspace: %s", param); return FALSE; } GST_INFO_OBJECT (y4mdec, "Parsed format as %s", gst_video_format_to_string (format)); continue; case 'W': if ((width = g_ascii_strtoull (param_value, NULL, 10)) == 0) { GST_ERROR_OBJECT (y4mdec, "Failed to parse width: %s", param); return FALSE; } continue; case 'H': if ((height = g_ascii_strtoull (param_value, NULL, 10)) == 0) { GST_ERROR_OBJECT (y4mdec, "Failed to parse height: %s", param); return FALSE; } continue; case 'I': if ((interlaced_char = param_value[0]) == 0) { GST_ERROR_OBJECT (y4mdec, "Expecting interlaced flag: %s", param); return FALSE; } continue; case 'F': if (!parse_ratio (param_value, &fps_n, &fps_d)) { GST_ERROR_OBJECT (y4mdec, "Failed to parse framerate: %s", param); return FALSE; } continue; case 'A': if (!parse_ratio (param_value, &par_n, &par_d)) { GST_ERROR_OBJECT (y4mdec, "Failed to parse PAR: %s", param); return FALSE; } continue; } /* TODO: parse common metadata ('X') such as YSCSS and color range */ GST_WARNING_OBJECT (y4mdec, "Unknown y4m param field '%s', ignoring", param); } g_strfreev (params); if (width > MAX_SIZE || height > MAX_SIZE) { GST_ERROR_OBJECT (y4mdec, "Dimensions %lux%lu out of range", width, height); return FALSE; } switch (interlaced_char) { case 0: case '?': case 'p': interlace_mode = GST_VIDEO_INTERLACE_MODE_PROGRESSIVE; break; case 't': case 'b': interlace_mode = GST_VIDEO_INTERLACE_MODE_INTERLEAVED; break; default: GST_ERROR_OBJECT (y4mdec, "Unknown interlaced char '%c'", interlaced_char); return FALSE; break; } if (fps_n == 0) fps_n = 1; if (fps_d == 0) fps_d = 1; if (par_n == 0) par_n = 1; if (par_d == 0) par_d = 1; gst_video_info_set_interlaced_format (&y4mdec->out_info, format, interlace_mode, width, height); GST_VIDEO_INFO_FPS_N (&y4mdec->out_info) = fps_n; GST_VIDEO_INFO_FPS_D (&y4mdec->out_info) = fps_d; GST_VIDEO_INFO_PAR_N (&y4mdec->out_info) = par_n; GST_VIDEO_INFO_PAR_D (&y4mdec->out_info) = par_d; if (!gst_y4m_video_unpadded_info (&y4mdec->info, &y4mdec->out_info)) return FALSE; y4mdec->passthrough = gst_video_info_is_equal (&y4mdec->info, &y4mdec->out_info); return TRUE; } static inline gboolean gst_y4m_dec_negotiate_pool (GstY4mDec * y4mdec, GstCaps * caps) { GstBaseParse *parse = GST_BASE_PARSE (y4mdec); GstBufferPool *pool = NULL; GstAllocator *allocator = NULL; GstAllocationParams params = { 0, }; GstStructure *config; guint size = GST_VIDEO_INFO_SIZE (&y4mdec->out_info), min = 0, max = 0; GstQuery *query; gboolean our_pool = FALSE, ret; if (y4mdec->pool) { gst_buffer_pool_set_active (y4mdec->pool, FALSE); gst_object_unref (y4mdec->pool); } y4mdec->pool = NULL; y4mdec->has_video_meta = FALSE; query = gst_query_new_allocation (caps, FALSE); if (gst_pad_peer_query (GST_BASE_PARSE_SRC_PAD (parse), query)) { y4mdec->has_video_meta = gst_query_find_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL); if (gst_query_get_n_allocation_params (query) > 0) { gst_query_parse_nth_allocation_param (query, 0, &allocator, ¶ms); } if (gst_query_get_n_allocation_pools (query) > 0) { gst_query_parse_nth_allocation_pool (query, 0, &pool, &size, &min, &max); size = MAX (size, GST_VIDEO_INFO_SIZE (&y4mdec->out_info)); } } do { if (!pool) { pool = gst_video_buffer_pool_new (); our_pool = TRUE; } config = gst_buffer_pool_get_config (pool); gst_buffer_pool_config_set_params (config, caps, size, min, max); gst_buffer_pool_config_set_allocator (config, allocator, ¶ms); gst_clear_object (&allocator); ret = gst_buffer_pool_set_config (pool, config); if (!ret) { if (our_pool) { GST_ERROR_OBJECT (y4mdec, "pool %" GST_PTR_FORMAT " doesn't accept configuration", pool); gst_clear_object (&pool); /* bail */ break; } else { GST_WARNING_OBJECT (y4mdec, "pool %" GST_PTR_FORMAT "doesn't accept " "configuration. Trying an internal pool", pool); max = min = 0; gst_clear_object (&pool); } } } while (!ret); y4mdec->pool = pool; gst_query_unref (query); gst_caps_unref (caps); return ret; } static inline gboolean gst_y4m_dec_negotiate (GstY4mDec * y4mdec) { GstBaseParse *parse = GST_BASE_PARSE (y4mdec); GstCaps *caps; caps = gst_video_info_to_caps (&y4mdec->out_info); if (!gst_pad_set_caps (GST_BASE_PARSE_SRC_PAD (parse), caps)) { GST_ERROR_OBJECT (y4mdec, "Failed to set caps in src pad: %" GST_PTR_FORMAT, caps); gst_caps_unref (caps); return FALSE; } return gst_y4m_dec_negotiate_pool (y4mdec, caps); } static inline gboolean gst_y4m_dec_parse_magic (GstY4mDec * y4mdec, gpointer data, gsize size, char header[MAX_STREAM_HEADER_LENGTH + 10]) { GstByteReader br; guint i; gst_byte_reader_init (&br, data, size); /* what ever until '\n' */ for (i = 0; i < MAX_STREAM_HEADER_LENGTH; i++) { header[i] = gst_byte_reader_get_uint8_unchecked (&br); if (header[i] == '\n') { header[i] = 0; break; } } if (i == MAX_STREAM_HEADER_LENGTH) { GST_ERROR_OBJECT (y4mdec, "Y4M header is too large"); return FALSE; } return TRUE; } static gboolean gst_y4m_dec_process_header (GstY4mDec * y4mdec, GstBuffer * buffer, gint * skipsize) { GstMapInfo mapinfo; char stream_hdr[MAX_STREAM_HEADER_LENGTH + 10]; gboolean ret = FALSE; if (!gst_buffer_map (buffer, &mapinfo, GST_MAP_READ)) { GST_ERROR_OBJECT (y4mdec, "Cannot map input buffer"); return FALSE; } g_assert (mapinfo.size >= MAX_STREAM_HEADER_LENGTH); if (!gst_y4m_dec_parse_magic (y4mdec, mapinfo.data, mapinfo.size, stream_hdr)) goto bail; if (!gst_y4m_dec_parse_header (y4mdec, stream_hdr)) goto bail; if (!gst_y4m_dec_negotiate (y4mdec)) goto bail; ret = TRUE; /* let's skip the stream header + '\n' */ *skipsize = strlen (stream_hdr) + 1; bail: gst_buffer_unmap (buffer, &mapinfo); return ret; } enum { FRAME_NOT_FOUND = -1, HEADER_RESYNC = -2, }; inline static gint gst_y4m_dec_frame_hdr_len (GstY4mDec * y4mdec, GstBuffer * buffer, gsize * buffer_size) { GstMapInfo mapinfo; char frame_hdr[MAX_STREAM_HEADER_LENGTH + 10]; gint ret = FRAME_NOT_FOUND; if (!gst_buffer_map (buffer, &mapinfo, GST_MAP_READ)) return FALSE; if (!gst_y4m_dec_parse_magic (y4mdec, mapinfo.data, mapinfo.size, frame_hdr)) goto bail; if (strncmp (frame_hdr, Y4M_FRAME_MAGIC, Y4M_FRAME_MAGIC_LEN) != 0) { if (strncmp (frame_hdr, Y4M_STREAM_MAGIC, Y4M_STREAM_MAGIC_LEN) == 0) { return HEADER_RESYNC; } else { GST_ERROR_OBJECT (y4mdec, "Frame header not found"); goto bail; } } /* FRAME + '\n' */ ret = strlen (frame_hdr) + 1; *buffer_size = mapinfo.size; bail: gst_buffer_unmap (buffer, &mapinfo); return ret; } static GstFlowReturn gst_y4m_dec_copy_buffer (GstY4mDec * y4mdec, GstBuffer * in_buffer, GstBuffer ** out_buffer_ptr) { GstVideoFrame in_frame, out_frame; GstFlowReturn ret; gboolean copied; GstBuffer *out_buffer = NULL; /* do memcpy hopefully in next element's pool */ g_assert (y4mdec->pool); if (!gst_buffer_pool_set_active (y4mdec->pool, TRUE)) { GST_ERROR_OBJECT (y4mdec, "Cannot activate internal pool"); goto error; } ret = gst_buffer_pool_acquire_buffer (y4mdec->pool, &out_buffer, NULL); if (ret != GST_FLOW_OK) { return ret; } if (!gst_video_frame_map (&in_frame, &y4mdec->info, in_buffer, GST_MAP_READ)) { GST_ERROR_OBJECT (y4mdec, "Cannot map input frame"); goto error; } if (!gst_video_frame_map (&out_frame, &y4mdec->out_info, out_buffer, GST_MAP_WRITE)) { GST_ERROR_OBJECT (y4mdec, "Cannot map output frame"); gst_video_frame_unmap (&in_frame); goto error; } copied = gst_video_frame_copy (&out_frame, &in_frame); gst_video_frame_unmap (&out_frame); gst_video_frame_unmap (&in_frame); if (!copied) { GST_ERROR_OBJECT (y4mdec, "Cannot copy frame"); goto error; } *out_buffer_ptr = out_buffer; return GST_FLOW_OK; error: { if (out_buffer) gst_buffer_unref (out_buffer); return GST_FLOW_ERROR; } } static inline gboolean _buffer_memory_is_aligned (GstBuffer * buffer, gboolean * is_aligned) { GstMapInfo mapinfo; if (!gst_buffer_map (buffer, &mapinfo, GST_MAP_READ)) return FALSE; /* check for 4 bytes alignment required for raw video */ *is_aligned = ((guintptr) mapinfo.data & 3) == 0; gst_buffer_unmap (buffer, &mapinfo); return TRUE; } static GstFlowReturn gst_y4m_dec_handle_frame (GstBaseParse * parse, GstBaseParseFrame * frame, gint * skipsize) { GstY4mDec *y4mdec = GST_Y4M_DEC (parse); gsize frame_size, buffer_size = 0; gint frame_hdr_len; gboolean is_aligned; GST_TRACE_OBJECT (y4mdec, "frame %" GST_PTR_FORMAT, frame->buffer); do { switch (y4mdec->state) { case PARSER_STATE_NONE: /* TODO: find the header and dismiss previous garbage (orc-based memem?) */ if (!gst_y4m_dec_process_header (y4mdec, frame->buffer, skipsize)) goto error; /* reconfigure baseparse */ { gst_base_parse_set_frame_rate (parse, GST_VIDEO_INFO_FPS_N (&y4mdec->out_info), GST_VIDEO_INFO_FPS_D (&y4mdec->out_info), 0, 0); /* update min frame size to input size */ gst_base_parse_set_min_frame_size (parse, GST_VIDEO_INFO_SIZE (&y4mdec->info) + Y4M_FRAME_MAGIC_LEN + 1); } y4mdec->state = PARSER_STATE_GOT_HEADER; return GST_FLOW_OK; case PARSER_STATE_GOT_HEADER: case PARSER_STATE_GOT_FRAME: frame_hdr_len = gst_y4m_dec_frame_hdr_len (y4mdec, frame->buffer, &buffer_size); if (frame_hdr_len == FRAME_NOT_FOUND) goto error; if (frame_hdr_len == HEADER_RESYNC) { y4mdec->state = PARSER_STATE_NONE; break; } /* input frame size */ frame_size = GST_VIDEO_INFO_SIZE (&y4mdec->info); /* frame is incomplete */ if (frame_size > buffer_size - frame_hdr_len) return GST_FLOW_OK; y4mdec->state = PARSER_STATE_GOT_FRAME; /* remove frame-header */ gst_buffer_resize (frame->buffer, frame_hdr_len, frame_size); if (!_buffer_memory_is_aligned (frame->buffer, &is_aligned)) goto error; if (is_aligned && y4mdec->passthrough) { /* best case scenario */ frame->out_buffer = gst_buffer_ref (frame->buffer); } else if (is_aligned && y4mdec->has_video_meta) { /* delegate memcopy to next element */ gst_buffer_add_video_meta_full (frame->buffer, 0, GST_VIDEO_INFO_FORMAT (&y4mdec->out_info), GST_VIDEO_INFO_WIDTH (&y4mdec->out_info), GST_VIDEO_INFO_HEIGHT (&y4mdec->out_info), GST_VIDEO_INFO_N_PLANES (&y4mdec->out_info), y4mdec->info.offset, y4mdec->info.stride); frame->out_buffer = gst_buffer_ref (frame->buffer); } else { GstFlowReturn ret; ret = gst_y4m_dec_copy_buffer (y4mdec, frame->buffer, &frame->out_buffer); if (ret == GST_FLOW_ERROR) goto error; else if (ret != GST_FLOW_OK) return ret; } GST_DEBUG_OBJECT (y4mdec, "output frame %" GST_PTR_FORMAT, frame->out_buffer); return gst_base_parse_finish_frame (parse, frame, frame_hdr_len + frame_size); default: GST_ERROR_OBJECT (y4mdec, "Invalid parser state"); return GST_FLOW_ERROR; } } while (TRUE); error: { GST_ELEMENT_ERROR (y4mdec, STREAM, DECODE, ("Failed to parse YUV4MPEG header"), (NULL)); return GST_FLOW_ERROR; } } static gboolean gst_y4m_dec_sink_event (GstBaseParse * parse, GstEvent * event) { if (GST_EVENT_TYPE (event) == GST_EVENT_STREAM_START) { if (!gst_y4m_dec_reset (parse)) return FALSE; } return GST_BASE_PARSE_CLASS (parent_class)->sink_event (parse, event); } static gboolean gst_y4m_dec_sink_query (GstBaseParse * parse, GstQuery * query) { /* videoencoder (from y4menc) does allocation queries. Ignore them. */ if (GST_QUERY_TYPE (query) == GST_QUERY_ALLOCATION) return FALSE; return GST_BASE_PARSE_CLASS (parent_class)->sink_query (parse, query); } static gboolean gst_y4m_dec_src_event (GstBaseParse * parse, GstEvent * event) { /* reject reverse playback */ if (GST_EVENT_TYPE (event) == GST_EVENT_SEEK) { gdouble rate; gst_event_parse_seek (event, &rate, NULL, NULL, NULL, NULL, NULL, NULL); if (rate < 0.0) { GstY4mDec *y4mdec = GST_Y4M_DEC (parse); GST_ERROR_OBJECT (y4mdec, "Reverse playback is not supported"); return FALSE; } } return GST_BASE_PARSE_CLASS (parent_class)->src_event (parse, event); }