From 436d33b2881fa82d3ba080010652e8adcc951297 Mon Sep 17 00:00:00 2001 From: Jan Schmidt Date: Sat, 25 May 2019 22:08:05 +1000 Subject: [PATCH] splitmuxsink: add the ability to mux auxilliary video streams The primary video stream is used to select fragment cut points at keyframe boundaries. Auxilliary video streams may be broken up at any packet - so fragments may not start with a keyframe for those streams. --- gst/multifile/gstsplitmuxsink.c | 26 +++++--- gst/multifile/gstsplitmuxsink.h | 2 +- tests/check/elements/splitmux.c | 107 +++++++++++++++++++++++++------- 3 files changed, 104 insertions(+), 31 deletions(-) diff --git a/gst/multifile/gstsplitmuxsink.c b/gst/multifile/gstsplitmuxsink.c index ab9fa00ebd..1db6a45f9d 100644 --- a/gst/multifile/gstsplitmuxsink.c +++ b/gst/multifile/gstsplitmuxsink.c @@ -1,5 +1,5 @@ /* GStreamer Muxer bin that splits output stream by size/time - * Copyright (C) <2014> Jan Schmidt + * Copyright (C) <2014-2019> Jan Schmidt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -146,6 +146,11 @@ GST_STATIC_PAD_TEMPLATE ("video", GST_PAD_SINK, GST_PAD_REQUEST, GST_STATIC_CAPS_ANY); +static GstStaticPadTemplate video_aux_sink_template = +GST_STATIC_PAD_TEMPLATE ("video_aux_%u", + GST_PAD_SINK, + GST_PAD_REQUEST, + GST_STATIC_CAPS_ANY); static GstStaticPadTemplate audio_sink_template = GST_STATIC_PAD_TEMPLATE ("audio_%u", GST_PAD_SINK, @@ -264,6 +269,8 @@ gst_splitmux_sink_class_init (GstSplitMuxSinkClass * klass) gst_element_class_add_static_pad_template (gstelement_class, &video_sink_template); + gst_element_class_add_static_pad_template (gstelement_class, + &video_aux_sink_template); gst_element_class_add_static_pad_template (gstelement_class, &audio_sink_template); gst_element_class_add_static_pad_template (gstelement_class, @@ -2580,7 +2587,7 @@ gst_splitmux_sink_request_new_pad (GstElement * element, GstElement *q; GstPad *q_sink = NULL, *q_src = NULL; gchar *gname, *qname; - gboolean is_video = FALSE; + gboolean is_primary_video = FALSE; MqStreamCtx *ctx; GST_DEBUG_OBJECT (element, "templ:%s, name:%s", templ->name_template, name); @@ -2591,8 +2598,10 @@ gst_splitmux_sink_request_new_pad (GstElement * element, g_signal_emit (splitmux, signals[SIGNAL_MUXER_ADDED], 0, splitmux->muxer); if (templ->name_template) { - if (g_str_equal (templ->name_template, "video")) { - if (splitmux->have_video) + if (g_str_equal (templ->name_template, "video") || + g_str_has_prefix (templ->name_template, "video_aux_")) { + is_primary_video = g_str_equal (templ->name_template, "video"); + if (is_primary_video && splitmux->have_video) goto already_have_video; /* FIXME: Look for a pad template with matching caps, rather than by name */ @@ -2610,7 +2619,6 @@ gst_splitmux_sink_request_new_pad (GstElement * element, gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (splitmux->muxer), "video"); } - is_video = TRUE; name = NULL; } else { GST_DEBUG_OBJECT (element, "searching for pad-template with name '%s'", @@ -2620,7 +2628,7 @@ gst_splitmux_sink_request_new_pad (GstElement * element, (splitmux->muxer), templ->name_template); /* Fallback to find sink pad templates named 'audio' (flvmux) */ - if (!mux_template) { + if (!mux_template && g_str_has_prefix (templ->name_template, "audio_")) { GST_DEBUG_OBJECT (element, "searching for pad-template with name 'audio'"); mux_template = @@ -2677,7 +2685,7 @@ gst_splitmux_sink_request_new_pad (GstElement * element, goto fail; } - if (is_video) + if (is_primary_video) gname = g_strdup ("video"); else if (name == NULL) gname = gst_pad_get_name (res); @@ -2720,7 +2728,7 @@ gst_splitmux_sink_request_new_pad (GstElement * element, gst_pad_add_probe (q_src, GST_PAD_PROBE_TYPE_DATA_DOWNSTREAM | GST_PAD_PROBE_TYPE_EVENT_FLUSH, (GstPadProbeCallback) handle_mq_output, ctx, NULL); - if (is_video && splitmux->reference_ctx != NULL) { + if (is_primary_video && splitmux->reference_ctx != NULL) { splitmux->reference_ctx->is_reference = FALSE; splitmux->reference_ctx = NULL; } @@ -2745,7 +2753,7 @@ gst_splitmux_sink_request_new_pad (GstElement * element, g_free (gname); - if (is_video) + if (is_primary_video) splitmux->have_video = TRUE; gst_pad_set_active (res, TRUE); diff --git a/gst/multifile/gstsplitmuxsink.h b/gst/multifile/gstsplitmuxsink.h index a5a148e94f..8e78bf1a5b 100644 --- a/gst/multifile/gstsplitmuxsink.h +++ b/gst/multifile/gstsplitmuxsink.h @@ -1,5 +1,5 @@ /* GStreamer split muxer bin - * Copyright (C) 2014 Jan Schmidt + * Copyright (C) 2014-2019 Jan Schmidt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public diff --git a/tests/check/elements/splitmux.c b/tests/check/elements/splitmux.c index 25925e97fa..9dfd14e26a 100644 --- a/tests/check/elements/splitmux.c +++ b/tests/check/elements/splitmux.c @@ -188,7 +188,7 @@ receive_handoff (GstElement * object G_GNUC_UNUSED, GstBuffer * buf, static void test_playback (const gchar * in_pattern, GstClockTime exp_first_time, - GstClockTime exp_last_time) + GstClockTime exp_last_time, gboolean test_reverse) { GstMessage *msg; GstElement *pipeline; @@ -229,20 +229,22 @@ test_playback (const gchar * in_pattern, GstClockTime exp_first_time, "Expected end of playback range 3s, got %" GST_TIME_FORMAT, GST_TIME_ARGS (last_ts)); - /* Test backwards */ - seek_pipeline (pipeline, -1.0, 0, -1); - msg = run_pipeline (pipeline); - fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS); - gst_message_unref (msg); - /* Check we saw the entire range of values */ - fail_unless (first_ts == exp_first_time, - "Expected start of playback range %" GST_TIME_FORMAT - ", got %" GST_TIME_FORMAT, GST_TIME_ARGS (exp_first_time), - GST_TIME_ARGS (first_ts)); - fail_unless (last_ts == exp_last_time, - "Expected end of playback range %" GST_TIME_FORMAT - ", got %" GST_TIME_FORMAT, GST_TIME_ARGS (exp_last_time), - GST_TIME_ARGS (last_ts)); + if (test_reverse) { + /* Test backwards */ + seek_pipeline (pipeline, -1.0, 0, -1); + msg = run_pipeline (pipeline); + fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS); + gst_message_unref (msg); + /* Check we saw the entire range of values */ + fail_unless (first_ts == exp_first_time, + "Expected start of playback range %" GST_TIME_FORMAT + ", got %" GST_TIME_FORMAT, GST_TIME_ARGS (exp_first_time), + GST_TIME_ARGS (first_ts)); + fail_unless (last_ts == exp_last_time, + "Expected end of playback range %" GST_TIME_FORMAT + ", got %" GST_TIME_FORMAT, GST_TIME_ARGS (exp_last_time), + GST_TIME_ARGS (last_ts)); + } gst_object_unref (pipeline); } @@ -251,7 +253,7 @@ GST_START_TEST (test_splitmuxsrc) { gchar *in_pattern = g_build_filename (GST_TEST_FILES_PATH, "splitvideo*.ogg", NULL); - test_playback (in_pattern, 0, 3 * GST_SECOND); + test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE); g_free (in_pattern); } @@ -367,7 +369,62 @@ GST_START_TEST (test_splitmuxsink) fail_unless (count == 3, "Expected 3 output files, got %d", count); in_pattern = g_build_filename (tmpdir, "out*.ogg", NULL); - test_playback (in_pattern, 0, 3 * GST_SECOND); + test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE); + g_free (in_pattern); +} + +GST_END_TEST; + +GST_START_TEST (test_splitmuxsink_multivid) +{ + GstMessage *msg; + GstElement *pipeline; + GstElement *sink; + gchar *dest_pattern; + guint count; + gchar *in_pattern; + + /* This pipeline should start a new file every GOP, ie 1 second, + * driven by the primary video stream and with 2 auxilliary video streams */ + pipeline = + gst_parse_launch + ("splitmuxsink name=splitsink " + " max-size-time=1000000 max-size-bytes=1000000 muxer=qtmux " + "videotestsrc num-buffers=15 ! video/x-raw,width=80,height=64,framerate=5/1 ! videoconvert !" + " queue ! vp8enc keyframe-max-dist=5 ! splitsink.video " + "videotestsrc num-buffers=15 pattern=snow ! video/x-raw,width=80,height=64,framerate=5/1 ! videoconvert !" + " queue ! vp8enc keyframe-max-dist=6 ! splitsink.video_aux_0 " + "videotestsrc num-buffers=15 pattern=ball ! video/x-raw,width=80,height=64,framerate=5/1 ! videoconvert !" + " queue ! vp8enc keyframe-max-dist=8 ! splitsink.video_aux_1 ", NULL); + fail_if (pipeline == NULL); + sink = gst_bin_get_by_name (GST_BIN (pipeline), "splitsink"); + fail_if (sink == NULL); + g_signal_connect (sink, "format-location-full", + (GCallback) check_format_location, NULL); + dest_pattern = g_build_filename (tmpdir, "out%05d.m4v", NULL); + g_object_set (G_OBJECT (sink), "location", dest_pattern, NULL); + g_free (dest_pattern); + g_object_unref (sink); + + msg = run_pipeline (pipeline); + + if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) + dump_error (msg); + fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS); + gst_message_unref (msg); + + gst_object_unref (pipeline); + + count = count_files (tmpdir); + fail_unless (count == 3, "Expected 3 output files, got %d", count); + + in_pattern = g_build_filename (tmpdir, "out*.m4v", NULL); + /* FIXME: Reverse playback works poorly with multiple video streams + * in qtdemux (at least, maybe other demuxers) at the time this was + * written, and causes test failures like buffers being output + * multiple times by qtdemux as it loops through GOPs. Disable that + * for now */ + test_playback (in_pattern, 0, 3 * GST_SECOND, FALSE); g_free (in_pattern); } @@ -431,7 +488,7 @@ GST_START_TEST (test_splitmuxsink_async) fail_unless (count == 3, "Expected 3 output files, got %d", count); in_pattern = g_build_filename (tmpdir, "matroska*.mkv", NULL); - test_playback (in_pattern, 0, 3 * GST_SECOND); + test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE); g_free (in_pattern); } @@ -713,7 +770,7 @@ GST_START_TEST (test_splitmuxsrc_caps_change) fail_unless (count == 2, "Expected 2 output files, got %d", count); in_pattern = g_build_filename (tmpdir, "out*.mp4", NULL); - test_playback (in_pattern, 0, GST_SECOND); + test_playback (in_pattern, 0, GST_SECOND, TRUE); g_free (in_pattern); } @@ -760,7 +817,7 @@ GST_START_TEST (test_splitmuxsrc_robust_mux) * reserved duration property. All we care about is that the muxing didn't fail because space ran out */ in_pattern = g_build_filename (tmpdir, "out*.mp4", NULL); - test_playback (in_pattern, 0, GST_SECOND); + test_playback (in_pattern, 0, GST_SECOND, TRUE); g_free (in_pattern); } @@ -802,7 +859,7 @@ splitmux_suite (void) TCase *tc_chain_complex = tcase_create ("complex"); TCase *tc_chain_mp4_jpeg = tcase_create ("caps_change"); gboolean have_theora, have_ogg, have_vorbis, have_matroska, have_qtmux, - have_jpeg; + have_jpeg, have_vp8; /* we assume that if encoder/muxer are there, decoder/demuxer will be a well */ have_theora = gst_registry_check_feature_version (gst_registry_get (), @@ -817,6 +874,8 @@ splitmux_suite (void) "qtmux", GST_VERSION_MAJOR, GST_VERSION_MINOR, 0); have_jpeg = gst_registry_check_feature_version (gst_registry_get (), "jpegenc", GST_VERSION_MAJOR, GST_VERSION_MINOR, 0); + have_vp8 = gst_registry_check_feature_version (gst_registry_get (), + "vp8enc", GST_VERSION_MAJOR, GST_VERSION_MINOR, 0); suite_add_tcase (s, tc_chain); suite_add_tcase (s, tc_chain_basic); @@ -854,6 +913,12 @@ splitmux_suite (void) } else { GST_INFO ("Skipping tests, missing plugins: jpegenc or mp4mux"); } + + if (have_qtmux && have_vp8) { + tcase_add_test (tc_chain, test_splitmuxsink_multivid); + } else { + GST_INFO ("Skipping tests, missing plugins: vp8enc or mp4mux"); + } return s; }