From 0efb792fb48a6910f266c50d51a330d1c13a9324 Mon Sep 17 00:00:00 2001 From: Hosang Lee Date: Tue, 16 Jun 2020 15:18:37 +0900 Subject: [PATCH] tests: qtdemux: add test for MSS fragment wrong data offset compensation A data offset with an offset smaller than the moof length is wrong in smooth streaming streams. The samples will not be located and eventually playback will error out. So compensate assuming data is in mdat following moof. Part-of: --- .../tests/check/elements/qtdemux.c | 214 ++++++++++++++++++ .../tests/files/mss-fragment.m4f | Bin 0 -> 14400 bytes 2 files changed, 214 insertions(+) create mode 100644 subprojects/gst-plugins-good/tests/files/mss-fragment.m4f diff --git a/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c b/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c index 011b060cfd..04092afff1 100644 --- a/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c +++ b/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c @@ -988,6 +988,218 @@ GST_START_TEST (test_qtdemux_pad_names) GST_END_TEST; +typedef struct +{ + GstPad *sinkpad; + guint sample_cnt; + guint expected_sample_cnt; +} MssModeTestData; + +static GstPadProbeReturn +qtdemux_probe_for_mss_mode (GstPad * pad, GstPadProbeInfo * info, + MssModeTestData * data) +{ + data->sample_cnt++; + GST_LOG ("samples received: %u", data->sample_cnt); + return GST_PAD_PROBE_OK; +} + +static void +qtdemux_pad_added_cb_in_mss_mode (GstElement * element, GstPad * pad, + MssModeTestData * data) +{ + GST_DEBUG_OBJECT (pad, "New pad added"); + + if (!data->sinkpad) { + GstPad *sinkpad = gst_pad_new_from_static_template (&sinktemplate, "sink"); + + gst_pad_set_event_function (sinkpad, _sink_event); + gst_pad_set_chain_function (sinkpad, _sink_chain); + + gst_pad_add_probe (sinkpad, GST_PAD_PROBE_TYPE_BUFFER, + (GstPadProbeCallback) qtdemux_probe_for_mss_mode, data, NULL); + gst_pad_set_active (sinkpad, TRUE); + data->sinkpad = sinkpad; + gst_pad_link (pad, sinkpad); + } +} + +/* Fragment taken from + * http://amssamples.streaming.mediaservices.windows.net/b6822ec8-5c2b-4ae0-a851-fd46a78294e9/ElephantsDream.ism/QualityLevels(53644)/Fragments(AAC_und_ch2_56kbps=0) + */ +#define MSS_FRAGMENT TEST_FILE_PREFIX "mss-fragment.m4f" +static guint8 *mss_fragment; +static const guint mss_fragment_len = 14400; + +GST_START_TEST (test_qtdemux_compensate_data_offset) +{ + /* Same fragment as above, but with modified trun box data offset field + * from 871 to 791 to mimic an mss fragment with data offset smaller + * than the moof size. */ + const guint mss_fragment_wrong_data_offset_len = 14400; + guint8 *mss_fragment_wrong_data_offset; + GstElement *qtdemux; + GstPad *sinkpad; + GstBuffer *inbuf; + GstSegment segment; + GstEvent *event; + GstCaps *caps; + GstCaps *mediacaps; + MssModeTestData data = { 0, }; + + data.expected_sample_cnt = 87; + + g_assert (load_file (MSS_FRAGMENT, &mss_fragment, mss_fragment_len)); + + /* Change trun box data offset field from 871 to 791 to mimic an MSS fragment + * with data offset smaller than the moof size. */ + mss_fragment_wrong_data_offset = g_memdup2 (mss_fragment, mss_fragment_len); + g_assert (GST_READ_UINT32_BE (&mss_fragment_wrong_data_offset[64]) == 871); + GST_WRITE_UINT32_BE (&mss_fragment_wrong_data_offset[64], 791); + + /* The goal of this test is to check that qtdemux can compensate + * wrong data offset in trun boxes and allow proper parsing of samples + * in mss mode. + */ + + qtdemux = gst_element_factory_make ("qtdemux", NULL); + gst_element_set_state (qtdemux, GST_STATE_PLAYING); + sinkpad = gst_element_get_static_pad (qtdemux, "sink"); + + /* We'll want to know when the source pad is added */ + g_signal_connect (qtdemux, "pad-added", (GCallback) + qtdemux_pad_added_cb_in_mss_mode, &data); + + /* Send the initial STREAM_START and segment (TIME) event */ + event = gst_event_new_stream_start ("TEST"); + GST_DEBUG ("Pushing stream-start event"); + fail_unless (gst_pad_send_event (sinkpad, event) == TRUE); + + /* Send CAPS event* */ + mediacaps = gst_caps_new_simple ("audio/mpeg", + "mpegversion", G_TYPE_INT, 4, + "channels", G_TYPE_INT, 2, "rate", G_TYPE_INT, 48000, NULL); + caps = + gst_caps_new_simple ("video/quicktime", "variant", G_TYPE_STRING, + "mss-fragmented", "timescale", G_TYPE_UINT64, 10000000, "media-caps", + GST_TYPE_CAPS, mediacaps, NULL); + + /* Send segment event* */ + event = gst_event_new_caps (caps); + GST_DEBUG ("Pushing caps event"); + fail_unless (gst_pad_send_event (sinkpad, event) == TRUE); + gst_caps_unref (mediacaps); + gst_caps_unref (caps); + + gst_segment_init (&segment, GST_FORMAT_TIME); + event = gst_event_new_segment (&segment); + GST_DEBUG ("Pushing segment event"); + fail_unless (gst_pad_send_event (sinkpad, event) == TRUE); + + /* Send the first fragment */ + /* NOTE: mss streams don't have moov */ + inbuf = gst_buffer_new_and_alloc (mss_fragment_wrong_data_offset_len); + gst_buffer_fill (inbuf, 0, mss_fragment_wrong_data_offset, + mss_fragment_wrong_data_offset_len); + GST_BUFFER_PTS (inbuf) = 0; + GST_BUFFER_OFFSET (inbuf) = 0; + GST_BUFFER_FLAG_SET (inbuf, GST_BUFFER_FLAG_DISCONT); + GST_DEBUG ("Pushing fragment"); + fail_unless (gst_pad_chain (sinkpad, inbuf) == GST_FLOW_OK); + fail_if (data.sinkpad == NULL); + + /* If data offset has been compensated samples will be pushed as normal */ + fail_unless (data.sample_cnt == data.expected_sample_cnt); + + gst_object_unref (sinkpad); + gst_pad_set_active (data.sinkpad, FALSE); + gst_object_unref (data.sinkpad); + gst_element_set_state (qtdemux, GST_STATE_NULL); + gst_object_unref (qtdemux); + + g_free (mss_fragment_wrong_data_offset); + g_clear_pointer (&mss_fragment, (GDestroyNotify) g_free); +} + +GST_END_TEST; + +GST_START_TEST (test_qtdemux_mss_fragment) +{ + GstElement *qtdemux; + GstPad *sinkpad; + GstBuffer *inbuf; + GstSegment segment; + GstEvent *event; + GstCaps *caps; + GstCaps *mediacaps; + MssModeTestData data = { 0, }; + + data.expected_sample_cnt = 87; + + g_assert (load_file (MSS_FRAGMENT, &mss_fragment, mss_fragment_len)); + + /* The goal of this test is to check that qtdemux can handle a normal + * mss fragment. + */ + + qtdemux = gst_element_factory_make ("qtdemux", NULL); + gst_element_set_state (qtdemux, GST_STATE_PLAYING); + sinkpad = gst_element_get_static_pad (qtdemux, "sink"); + + /* We'll want to know when the source pad is added */ + g_signal_connect (qtdemux, "pad-added", (GCallback) + qtdemux_pad_added_cb_in_mss_mode, &data); + + /* Send the initial STREAM_START and segment (TIME) event */ + event = gst_event_new_stream_start ("TEST"); + GST_DEBUG ("Pushing stream-start event"); + fail_unless (gst_pad_send_event (sinkpad, event) == TRUE); + + /* Send CAPS event* */ + mediacaps = gst_caps_new_simple ("audio/mpeg", + "mpegversion", G_TYPE_INT, 4, + "channels", G_TYPE_INT, 2, "rate", G_TYPE_INT, 48000, NULL); + caps = + gst_caps_new_simple ("video/quicktime", "variant", G_TYPE_STRING, + "mss-fragmented", "timescale", G_TYPE_UINT64, 10000000, "media-caps", + GST_TYPE_CAPS, mediacaps, NULL); + + /* Send segment event* */ + event = gst_event_new_caps (caps); + GST_DEBUG ("Pushing caps event"); + fail_unless (gst_pad_send_event (sinkpad, event) == TRUE); + gst_caps_unref (mediacaps); + gst_caps_unref (caps); + + gst_segment_init (&segment, GST_FORMAT_TIME); + event = gst_event_new_segment (&segment); + GST_DEBUG ("Pushing segment event"); + fail_unless (gst_pad_send_event (sinkpad, event) == TRUE); + + /* Send the first fragment */ + /* NOTE: mss streams don't have moov */ + inbuf = gst_buffer_new_and_alloc (mss_fragment_len); + gst_buffer_fill (inbuf, 0, mss_fragment, mss_fragment_len); + GST_BUFFER_PTS (inbuf) = 0; + GST_BUFFER_OFFSET (inbuf) = 0; + GST_BUFFER_FLAG_SET (inbuf, GST_BUFFER_FLAG_DISCONT); + GST_DEBUG ("Pushing fragment"); + fail_unless (gst_pad_chain (sinkpad, inbuf) == GST_FLOW_OK); + fail_if (data.sinkpad == NULL); + + fail_unless (data.sample_cnt == data.expected_sample_cnt); + + gst_object_unref (sinkpad); + gst_pad_set_active (data.sinkpad, FALSE); + gst_object_unref (data.sinkpad); + gst_element_set_state (qtdemux, GST_STATE_NULL); + gst_object_unref (qtdemux); + + g_clear_pointer (&mss_fragment, (GDestroyNotify) g_free); +} + +GST_END_TEST; + static Suite * qtdemux_suite (void) { @@ -1001,6 +1213,8 @@ qtdemux_suite (void) tcase_add_test (tc_chain, test_qtdemux_duplicated_moov); tcase_add_test (tc_chain, test_qtdemux_stream_change); tcase_add_test (tc_chain, test_qtdemux_pad_names); + tcase_add_test (tc_chain, test_qtdemux_compensate_data_offset); + tcase_add_test (tc_chain, test_qtdemux_mss_fragment); return s; } diff --git a/subprojects/gst-plugins-good/tests/files/mss-fragment.m4f b/subprojects/gst-plugins-good/tests/files/mss-fragment.m4f new file mode 100644 index 0000000000000000000000000000000000000000..6b0811ebba645850bafe4f0906e1a21d7d399dd3 GIT binary patch literal 14400 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 literal 0 HcmV?d00001