diff --git a/tests/check/Makefile.am b/tests/check/Makefile.am index 6dd8dec5a4..e558782d97 100644 --- a/tests/check/Makefile.am +++ b/tests/check/Makefile.am @@ -456,7 +456,7 @@ elements_dash_demux_LDADD = \ -lgstapp-$(GST_API_VERSION) \ $(top_builddir)/gst-libs/gst/adaptivedemux/libgstadaptivedemux-@GST_API_VERSION@.la -elements_dash_demux_SOURCES = elements/fake_http_src.c elements/fake_http_src.h elements/dash_demux.c +elements_dash_demux_SOURCES = elements/test_http_src.c elements/test_http_src.h elements/adaptive_demux_engine.c elements/adaptive_demux_engine.h elements/adaptive_demux_common.c elements/adaptive_demux_common.h elements/dash_demux.c pipelines_streamheader_CFLAGS = $(GIO_CFLAGS) $(AM_CFLAGS) pipelines_streamheader_LDADD = $(GIO_LIBS) $(LDADD) diff --git a/tests/check/elements/adaptive_demux_common.c b/tests/check/elements/adaptive_demux_common.c new file mode 100644 index 0000000000..67d724d82d --- /dev/null +++ b/tests/check/elements/adaptive_demux_common.c @@ -0,0 +1,454 @@ +/* A set of utility functions that are common between elements + * based upon GstAdaptiveDemux + * + * Copyright (c) <2015> YouView TV Ltd + * + * 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. + */ + +#include +#include "adaptive_demux_engine.h" +#include "adaptive_demux_common.h" + +#define GST_TEST_HTTP_SRC_NAME "testhttpsrc" + +struct _GstAdaptiveDemuxTestCaseClass +{ + GObjectClass parent_class; +}; + +#define gst_adaptive_demux_test_case_parent_class parent_class + +static void gst_adaptive_demux_test_case_dispose (GObject * object); +static void gst_adaptive_demux_test_case_finalize (GObject * object); +static void gst_adaptive_demux_test_case_clear (GstAdaptiveDemuxTestCase * + testData); + +G_DEFINE_TYPE (GstAdaptiveDemuxTestCase, gst_adaptive_demux_test_case, + G_TYPE_OBJECT); + +static void +gst_adaptive_demux_test_case_class_init (GstAdaptiveDemuxTestCaseClass * klass) +{ + GObjectClass *object = G_OBJECT_CLASS (klass); + + object->dispose = gst_adaptive_demux_test_case_dispose; + object->finalize = gst_adaptive_demux_test_case_finalize; +} + +static void +gst_adaptive_demux_test_case_init (GstAdaptiveDemuxTestCase * testData) +{ + testData->output_streams = NULL; + testData->test_task = NULL; + g_rec_mutex_init (&testData->test_task_lock); + g_mutex_init (&testData->test_task_state_lock); + g_cond_init (&testData->test_task_state_cond); + gst_adaptive_demux_test_case_clear (testData); +} + +static void +gst_adaptive_demux_test_case_clear (GstAdaptiveDemuxTestCase * testData) +{ + if (testData->output_streams) { + g_list_free (testData->output_streams); + testData->output_streams = NULL; + } + testData->count_of_finished_streams = 0; + if (testData->test_task) { + gst_task_stop (testData->test_task); + gst_task_join (testData->test_task); + gst_object_unref (testData->test_task); + testData->test_task = NULL; + } + testData->signal_context = NULL; + testData->test_task_state = TEST_TASK_STATE_NOT_STARTED; + testData->threshold_for_seek = 0; + testData->signal_context = NULL; +} + + +static void +gst_adaptive_demux_test_case_dispose (GObject * object) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (object); + + gst_adaptive_demux_test_case_clear (testData); + + GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object)); +} + +static void +gst_adaptive_demux_test_case_finalize (GObject * object) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (object); + + g_cond_clear (&testData->test_task_state_cond); + g_mutex_clear (&testData->test_task_state_lock); + g_rec_mutex_clear (&testData->test_task_lock); + if (testData->test_task) { + gst_task_stop (testData->test_task); + gst_task_join (testData->test_task); + gst_object_unref (testData->test_task); + testData->test_task = NULL; + } + if (testData->output_streams) { + g_list_free (testData->output_streams); + testData->output_streams = NULL; + } + + GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); +} + +/** + * gst_adaptive_demux_test_case_new: + * + * Creates a new #GstAdaptiveDemuxTestCase. Free with g_object_unref(). + * + * Returns: (transfer full): a new #GstAdaptiveDemuxTestCase + */ +GstAdaptiveDemuxTestCase * +gst_adaptive_demux_test_case_new (void) +{ + return g_object_newv (GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE, 0, NULL); +} + + +GstAdaptiveDemuxTestExpectedOutput * +gst_adaptive_demux_test_find_test_data_by_stream (GstAdaptiveDemuxTestCase * + testData, GstAdaptiveDemuxTestOutputStream * stream, guint * index) +{ + gchar *pad_name; + GstAdaptiveDemuxTestExpectedOutput *ret = NULL; + guint count = 0; + + pad_name = gst_pad_get_name (stream->pad); + fail_unless (pad_name != NULL); + for (GList * walk = testData->output_streams; walk; walk = g_list_next (walk)) { + GstAdaptiveDemuxTestExpectedOutput *td = walk->data; + if (strcmp (td->name, pad_name) == 0) { + ret = td; + if (index) + *index = count; + } + ++count; + } + g_free (pad_name); + return ret; +} + +/* function to validate data received by AppSink */ +gboolean +gst_adaptive_demux_test_check_received_data (GstAdaptiveDemuxTestEngine * + engine, GstAdaptiveDemuxTestOutputStream * stream, GstBuffer * buffer, + gpointer user_data) +{ + GstMapInfo info; + guint pattern; + guint64 streamOffset; + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + GstAdaptiveDemuxTestExpectedOutput *testOutputStreamData; + + fail_unless (stream != NULL); + fail_unless (engine->pipeline != NULL); + testOutputStreamData = + gst_adaptive_demux_test_find_test_data_by_stream (testData, stream, NULL); + fail_unless (testOutputStreamData != NULL); + streamOffset = stream->segment_start + stream->segment_received_size; + if (testOutputStreamData->expected_data) { + gsize size = gst_buffer_get_size (buffer); + if (gst_buffer_memcmp (buffer, 0, + &testOutputStreamData->expected_data[streamOffset], size) == 0) { + return TRUE; + } + /* If buffers do not match, fall back to a slower byte-based check + so that the test can output the position where the received data + diverges from expected_data + */ + } + + gst_buffer_map (buffer, &info, GST_MAP_READ); + + GST_DEBUG + ("segment_start = %" G_GUINT64_FORMAT " segment_received_size = %" + G_GUINT64_FORMAT " bufferSize=%d", + stream->segment_start, stream->segment_received_size, (gint) info.size); + + pattern = streamOffset - streamOffset % sizeof (pattern); + for (guint64 i = 0; i != info.size; ++i) { + guint received = info.data[i]; + guint expected; + + if (testOutputStreamData->expected_data) { + fail_unless (streamOffset + i < testOutputStreamData->expected_size); + expected = testOutputStreamData->expected_data[streamOffset + i]; + } else { + gchar pattern_byte_to_read; + + pattern_byte_to_read = (streamOffset + i) % sizeof (pattern); + if (pattern_byte_to_read == 0) { + pattern = streamOffset + i; + } + + expected = (pattern >> (pattern_byte_to_read * 8)) & 0xFF; +#if 0 + GST_DEBUG + ("received '0x%02x' expected '0x%02x' offset %" G_GUINT64_FORMAT + " pattern=%08x byte_to_read=%d", + received, expected, i, pattern, pattern_byte_to_read); +#endif + } + + fail_unless (received == expected, + "output validation failed: received '0x%02x' expected '0x%02x' byte %" + G_GUINT64_FORMAT " offset=%" G_GUINT64_FORMAT "\n", received, expected, + i, streamOffset); + } + + gst_buffer_unmap (buffer, &info); + return TRUE; +} + +/* function to check total size of data received by AppSink + * will be called when AppSink receives eos. + */ +void +gst_adaptive_demux_test_check_size_of_received_data (GstAdaptiveDemuxTestEngine + * engine, GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + GstAdaptiveDemuxTestExpectedOutput *testOutputStreamData; + + testOutputStreamData = + gst_adaptive_demux_test_find_test_data_by_stream (testData, stream, NULL); + fail_unless (testOutputStreamData != NULL); + + fail_unless (stream->total_received_size == + testOutputStreamData->expected_size, + "size validation failed, expected %d received %d", + testOutputStreamData->expected_size, stream->total_received_size); + testData->count_of_finished_streams++; + if (testData->count_of_finished_streams == + g_list_length (testData->output_streams)) { + g_main_loop_quit (engine->loop); + } +} + +typedef struct _SeekTaskContext +{ + GstElement *pipeline; + GstTask *task; +} SeekTaskContext; + +/* function to generate a seek event. Will be run in a separate thread */ +static void +testSeekTaskDoSeek (gpointer user_data) +{ + SeekTaskContext *context = (SeekTaskContext *) user_data; + GstTask *task; + + GST_DEBUG ("testSeekTaskDoSeek calling seek"); + + /* seek to 5ms. + * Because there is only one fragment, we expect the whole file to be + * downloaded again + */ + if (!gst_element_seek_simple (GST_ELEMENT (context->pipeline), + GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, + 5 * GST_MSECOND)) { + fail ("Seek failed!\n"); + } + GST_DEBUG ("seek ok"); + task = context->task; + g_slice_free (SeekTaskContext, context); + gst_task_stop (task); +} + +/* function to be called during seek test when demux sends data to AppSink + * It monitors the data sent and after a while will generate a seek request. + */ +static gboolean +testSeekAdaptiveDemuxSendsData (GstAdaptiveDemuxTestEngine * engine, + GstAdaptiveDemuxTestOutputStream * stream, + GstBuffer * buffer, gpointer user_data) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + SeekTaskContext *seekContext; + GstAdaptiveDemuxTestExpectedOutput *testOutputStreamData; + guint index = 0; + + testOutputStreamData = + gst_adaptive_demux_test_find_test_data_by_stream (testData, stream, + &index); + fail_unless (testOutputStreamData != NULL); + /* first entry in testData->output_streams is the + PAD on which to perform the seek */ + if (index == 0 && + testData->test_task == NULL && + (stream->total_received_size + stream->segment_received_size) >= + testData->threshold_for_seek) { + testData->threshold_for_seek = + stream->total_received_size + stream->segment_received_size; + + /* the seek will be to the beginning of the file, so expect to receive + * threshold_for_seek + a whole file + */ + testOutputStreamData->expected_size += testData->threshold_for_seek; + + GST_DEBUG ("starting seek task"); + + g_mutex_lock (&testData->test_task_state_lock); + testData->test_task_state = + TEST_TASK_STATE_WAITING_FOR_TESTSRC_STATE_CHANGE; + g_mutex_unlock (&testData->test_task_state_lock); + + seekContext = g_slice_new (SeekTaskContext); + seekContext->pipeline = engine->pipeline; + testData->test_task = seekContext->task = + gst_task_new ((GstTaskFunction) testSeekTaskDoSeek, seekContext, NULL); + gst_task_set_lock (testData->test_task, &testData->test_task_lock); + gst_task_start (testData->test_task); + + GST_DEBUG ("seek task started"); + + g_mutex_lock (&testData->test_task_state_lock); + + GST_DEBUG ("waiting for seek task to change state on testsrc"); + + /* wait for test_task to run, send a flush start event to AppSink + * and change the testhttpsrc element state from PLAYING to PAUSED + */ + while (testData->test_task_state == + TEST_TASK_STATE_WAITING_FOR_TESTSRC_STATE_CHANGE) { + g_cond_wait (&testData->test_task_state_cond, + &testData->test_task_state_lock); + } + g_mutex_unlock (&testData->test_task_state_lock); + /* we can continue now, but this buffer will be rejected by AppSink + * because it is in flushing mode + */ + GST_DEBUG ("seek task changed state on testsrc, resuming"); + } + + return TRUE; +} + +/* callback called when main_loop detects a state changed event */ +static void +testSeekOnStateChanged (GstBus * bus, GstMessage * msg, gpointer user_data) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + GstState old_state, new_state; + const char *srcName = GST_OBJECT_NAME (msg->src); + + gst_message_parse_state_changed (msg, &old_state, &new_state, NULL); + GST_DEBUG ("Element %s changed state from %s to %s", + GST_OBJECT_NAME (msg->src), + gst_element_state_get_name (old_state), + gst_element_state_get_name (new_state)); + + if (strstr (srcName, "bin") == srcName && + old_state == GST_STATE_PLAYING && new_state == GST_STATE_PAUSED) { + g_mutex_lock (&testData->test_task_state_lock); + if (testData->test_task_state == + TEST_TASK_STATE_WAITING_FOR_TESTSRC_STATE_CHANGE) { + GST_DEBUG ("changing test_task_state"); + testData->test_task_state = TEST_TASK_STATE_EXITING; + g_cond_signal (&testData->test_task_state_cond); + } + g_mutex_unlock (&testData->test_task_state_lock); + } +} + +/* + * Issue a seek request after media segment has started to be downloaded + * on the first pad listed in GstAdaptiveDemuxTestOutputStreamData and the + * first chunk of at least one byte has already arrived in AppSink + */ +static void +testSeekPreTestCallback (GstAdaptiveDemuxTestEngine * engine, + gpointer user_data) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + GstBus *bus; + + /* register a callback to listen for state change events */ + bus = gst_pipeline_get_bus (GST_PIPELINE (engine->pipeline)); + gst_bus_add_signal_watch (bus); + g_signal_connect (bus, "message::state-changed", + G_CALLBACK (testSeekOnStateChanged), testData); +} + +/* function to check total size of data received by AppSink + * will be called when AppSink receives eos. + */ +void gst_adaptive_demux_test_download_error_size_of_received_data + (GstAdaptiveDemuxTestEngine * engine, + GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + GstAdaptiveDemuxTestExpectedOutput *testOutputStreamData; + + testOutputStreamData = + gst_adaptive_demux_test_find_test_data_by_stream (testData, stream, NULL); + fail_unless (testOutputStreamData != NULL); + /* expect to receive more than 0 */ + fail_unless (stream->total_received_size > 0, + "size validation failed for %s, expected > 0, received %d", + testOutputStreamData->name, stream->total_received_size); + + /* expect to receive less than file size */ + fail_unless (stream->total_received_size < + testOutputStreamData->expected_size, + "size validation failed for %s, expected < %d received %d", + testOutputStreamData->name, testOutputStreamData->expected_size, + stream->total_received_size); + if (testData->count_of_finished_streams == + g_list_length (testData->output_streams)) { + g_main_loop_quit (engine->loop); + } +} + +void +gst_adaptive_demux_test_seek (const gchar * element_name, + const gchar * manifest_uri, GstAdaptiveDemuxTestCase * testData) +{ + GstAdaptiveDemuxTestCallbacks cb = { 0 }; + cb.appsink_received_data = gst_adaptive_demux_test_check_received_data; + cb.appsink_eos = gst_adaptive_demux_test_check_size_of_received_data; + cb.pre_test = testSeekPreTestCallback; + cb.demux_sent_data = testSeekAdaptiveDemuxSendsData; + gst_adaptive_demux_test_run (element_name, manifest_uri, &cb, testData); + /* the call to g_object_unref of testData will clean up the seek task */ +} + +void +gst_adaptive_demux_test_setup (void) +{ + GstRegistry *registry; + gboolean ret; + + registry = gst_registry_get (); + ret = gst_test_http_src_register_plugin (registry, GST_TEST_HTTP_SRC_NAME); + fail_unless (ret); +} + +void +gst_adaptive_demux_test_teardown (void) +{ + gst_test_http_src_install_callbacks (NULL, NULL); + gst_test_http_src_set_default_blocksize (0); +} diff --git a/tests/check/elements/adaptive_demux_common.h b/tests/check/elements/adaptive_demux_common.h new file mode 100644 index 0000000000..3969582b3e --- /dev/null +++ b/tests/check/elements/adaptive_demux_common.h @@ -0,0 +1,208 @@ +/* A set of utility functions that are common between elements + * based upon GstAdaptiveDemux + * + * Copyright (c) <2015> YouView TV Ltd + * + * 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. + */ + +#ifndef __GST_ADAPTIVE_DEMUX_COMMON_TEST_H__ +#define __GST_ADAPTIVE_DEMUX_COMMON_TEST_H__ + +#include +#include "adaptive_demux_engine.h" + +G_BEGIN_DECLS + +#define GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE \ + (gst_adaptive_demux_test_case_get_type()) +#define GST_ADAPTIVE_DEMUX_TEST_CASE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE, GstAdaptiveDemuxTestCase)) +#define GST_ADAPTIVE_DEMUX_TEST_CASE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE, GstAdaptiveDemuxTestCaseClass)) +#define GST_ADAPTIVE_DEMUX_TEST_CASE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE, GstAdaptiveDemuxTestCaseClass)) +#define GST_IS_ADAPTIVE_DEMUX_TEST_CASE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE)) +#define GST_IS_ADAPTIVE_DEMUX_TEST_CASE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_ADAPTIVE_DEMUX_TEST_CASE)) + +/** + * TestTaskState: + * The seek test uses a separate task to perform the seek operation. + * After starting the task, the caller blocks until the seek task + * flushes the AppSink and changes the GstFakeSoupHTTPSrc element state from + * PLAYING to PAUSED. + * When that event is detected, the caller is allowed to resume. + * Any data that will be sent to AppSink after resume will be rejected because + * AppSink is in flushing mode. + */ +typedef enum +{ + TEST_TASK_STATE_NOT_STARTED, + TEST_TASK_STATE_WAITING_FOR_TESTSRC_STATE_CHANGE, + TEST_TASK_STATE_EXITING, +} TestTaskState; + +/** + * GstAdaptiveDemuxTestExpectedOutput: + * Structure used to store output stream related data. + * It is used by the test during output validation. + * The fields are set by the testcase function prior + * to starting the test. + */ +typedef struct _GstAdaptiveDemuxTestExpectedOutput +{ + /* the name of the demux src pad generating this stream */ + const char *name; + /* the expected size on this stream */ + guint64 expected_size; + /* the expected data on this stream (optional) */ + const guint8* expected_data; +} GstAdaptiveDemuxTestExpectedOutput; + +typedef struct _GstAdaptiveDemuxTestCaseClass GstAdaptiveDemuxTestCaseClass; +typedef struct _GstAdaptiveDemuxTestCase +{ + GObject parent; + + /* output data used to validate the test + * list of GstAdaptiveDemuxTestExpectedOutput, one entry per stream + */ + GList *output_streams; /*GList*/ + + /* the number of streams that finished. + * Main thread will stop the pipeline when all streams are finished + * (i.e. count_of_finished_streams == g_list_length(output_streams) ) + */ + guint count_of_finished_streams; + + /* taskTesk... is a set of variables that can be used by a test + * that needs to perform operations from another thread + * For example, it is used by the seek test to perform the seek + * operation + */ + GstTask *test_task; + GRecMutex test_task_lock; + TestTaskState test_task_state; + GMutex test_task_state_lock; + GCond test_task_state_cond; + + /* seek test will wait for this amount of bytes to be sent by + * demux to AppSink before triggering a seek request + */ + guint64 threshold_for_seek; + + gpointer signal_context; +} GstAdaptiveDemuxTestCase; + +/* high-level unit test functions */ + +/** + * gst_adaptive_demux_test_setup: + * Causes the test HTTP src element to be registered + */ +void gst_adaptive_demux_test_setup (void); + +void gst_adaptive_demux_test_teardown (void); + +GType gst_adaptive_demux_test_case_get_type (void); + +/** + * gst_adaptive_demux_test_case_new: creates new #GstAdaptiveDemuxTestCase + * object. Use #g_object_unref to free. + */ +GstAdaptiveDemuxTestCase * gst_adaptive_demux_test_case_new (void) G_GNUC_MALLOC; + +/** + * gst_adaptive_demux_test_seek: test that element supports seeking + * @element_name: The name of the demux element (e.g. "dashdemux") + * @manifest_uri: The URI of the manifest to load + * @testData: The #GstAdaptiveDemuxTestCase that the test expects the + * demux element to produce. + * + * Creates a pipeline and starts it. Once data is flowing, request a + * seek to almost the start of the stream. + */ +void gst_adaptive_demux_test_seek (const gchar * element_name, + const gchar * manifest_uri, + GstAdaptiveDemuxTestCase *testData); + +/* Utility functions for use within a unit test */ + +/** + * gst_adaptive_demux_test_check_size_of_received_data: + * @engine: The #GstAdaptiveDemuxTestEngine that caused this callback + * @stream: The #GstAdaptiveDemuxTestOutputStream that caused this callback + * @user_data: A pointer to a #GstAdaptiveDemuxTestCase object + * + * This function can be used as an EOS callback to check that the + * size of received data equals expected_size. It should be used with + * a test that expects the entire file to be downloaded. + */ +void gst_adaptive_demux_test_check_size_of_received_data( + GstAdaptiveDemuxTestEngine *engine, + GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data); + +/** + * gst_adaptive_demux_test_download_error_size_of_received_data: + * @engine: The #GstAdaptiveDemuxTestEngine that caused this callback + * @stream: The #GstAdaptiveDemuxTestOutputStream that caused this callback + * @user_data: A pointer to a #GstAdaptiveDemuxTestCase object + * + * This function can be used as an EOS callback to check that the + * size of received data is >0 && YouView TV Ltd + * + * 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. + */ + +#include +#include "adaptive_demux_engine.h" + +typedef struct _GstAdaptiveDemuxTestEnginePrivate +{ + GstAdaptiveDemuxTestEngine engine; + const GstAdaptiveDemuxTestCallbacks *callbacks; + gpointer user_data; +} GstAdaptiveDemuxTestEnginePrivate; + + +#define GST_TEST_GET_LOCK(d) (&(((GstAdaptiveDemuxTestEnginePrivate*)(d))->engine.lock)) +#define GST_TEST_LOCK(d) g_mutex_lock (GST_TEST_GET_LOCK (d)) +#define GST_TEST_UNLOCK(d) g_mutex_unlock (GST_TEST_GET_LOCK (d)) + +static void +adaptive_demux_engine_stream_state_finalize (gpointer data) +{ + GstAdaptiveDemuxTestOutputStream *stream = + (GstAdaptiveDemuxTestOutputStream *) data; + if (stream->appsink) + gst_object_unref (stream->appsink); + if (stream->pad) + gst_object_unref (stream->pad); + if (stream->internal_pad) + gst_object_unref (stream->internal_pad); + g_slice_free (GstAdaptiveDemuxTestOutputStream, stream); +} + +/* get the testOutput entry in testData corresponding to the given AppSink */ +static GstAdaptiveDemuxTestOutputStream * +getTestOutputDataByAppsink (GstAdaptiveDemuxTestEnginePrivate * priv, + GstAppSink * appsink) +{ + for (guint i = 0; i < priv->engine.output_streams->len; ++i) { + GstAdaptiveDemuxTestOutputStream *state; + state = g_ptr_array_index (priv->engine.output_streams, i); + if (state->appsink == appsink) { + return state; + } + } + ck_abort_msg ("cannot find appsink %p in the output data", appsink); + return NULL; +} + +/* get the output stream entry in corresponding to the given Pad */ +static GstAdaptiveDemuxTestOutputStream * +getTestOutputDataByPad (GstAdaptiveDemuxTestEnginePrivate * priv, + GstPad * pad, gboolean abort_if_not_found) +{ + for (guint i = 0; i < priv->engine.output_streams->len; ++i) { + GstAdaptiveDemuxTestOutputStream *stream; + stream = g_ptr_array_index (priv->engine.output_streams, i); + if (stream->internal_pad == pad || stream->pad == pad) { + return stream; + } + } + if (abort_if_not_found) + ck_abort_msg ("cannot find pad %p in the output data", pad); + return NULL; +} + +/* callback called when AppSink receives data */ +static GstFlowReturn +on_appSinkNewSample (GstAppSink * appsink, gpointer user_data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) user_data; + GstAdaptiveDemuxTestEngine *engine; + GstAdaptiveDemuxTestOutputStream *testOutputStream = NULL; + GstSample *sample; + GstBuffer *buffer; + gboolean ret = TRUE; + + fail_unless (priv != NULL); + GST_TEST_LOCK (priv); + engine = &priv->engine; + testOutputStream = getTestOutputDataByAppsink (priv, appsink); + + sample = gst_app_sink_pull_sample (appsink); + fail_unless (sample != NULL); + buffer = gst_sample_get_buffer (sample); + fail_unless (buffer != NULL); + + /* call the test callback, if registered */ + if (priv->callbacks->appsink_received_data) + ret = priv->callbacks->appsink_received_data (engine, + testOutputStream, buffer, priv->user_data); + + testOutputStream->segment_received_size += gst_buffer_get_size (buffer); + + gst_sample_unref (sample); + + GST_TEST_UNLOCK (priv); + + if (!ret) + return GST_FLOW_EOS; + + return GST_FLOW_OK; +} + +/* callback called when AppSink receives eos */ +static void +on_appSinkEOS (GstAppSink * appsink, gpointer user_data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) user_data; + GstAdaptiveDemuxTestOutputStream *testOutputStream = NULL; + + fail_unless (priv != NULL); + GST_TEST_LOCK (priv); + testOutputStream = getTestOutputDataByAppsink (priv, appsink); + + testOutputStream->total_received_size += + testOutputStream->segment_received_size; + testOutputStream->segment_received_size = 0; + + if (priv->callbacks->appsink_eos) + priv->callbacks->appsink_eos (&priv->engine, + testOutputStream, priv->user_data); + + GST_TEST_UNLOCK (priv); +} + +/* callback called when demux sends data to AppSink */ +static GstPadProbeReturn +on_demux_sent_data (GstPad * pad, GstPadProbeInfo * info, gpointer data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) data; + GstAdaptiveDemuxTestOutputStream *stream = NULL; + GstBuffer *buffer; + + buffer = GST_PAD_PROBE_INFO_BUFFER (info); + + GST_TEST_LOCK (priv); + stream = getTestOutputDataByPad (priv, pad, TRUE); + + if (priv->callbacks->demux_sent_data) { + (*priv->callbacks->demux_sent_data) (&priv->engine, + stream, buffer, priv->user_data); + } + + GST_TEST_UNLOCK (priv); + return GST_PAD_PROBE_OK; +} + +/* callback called when demux receives events from GstFakeSoupHTTPSrc */ +static GstPadProbeReturn +on_demuxReceivesEvent (GstPad * pad, GstPadProbeInfo * info, gpointer data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) data; + GstAdaptiveDemuxTestOutputStream *stream = NULL; + GstEvent *event; + const GstSegment *segment; + + event = GST_PAD_PROBE_INFO_EVENT (info); + GST_DEBUG ("Received event %" GST_PTR_FORMAT " on pad %" GST_PTR_FORMAT, + event, pad); + + if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) { + /* a new segment will start arriving + * update segment_start used by pattern validation + */ + gst_event_parse_segment (event, &segment); + + GST_TEST_LOCK (priv); + stream = getTestOutputDataByPad (priv, pad, TRUE); + stream->total_received_size += stream->segment_received_size; + stream->segment_received_size = 0; + stream->segment_start = segment->start; + GST_TEST_UNLOCK (priv); + } else if (GST_EVENT_TYPE (event) == GST_EVENT_EOS) { + GST_TEST_LOCK (priv); + stream = getTestOutputDataByPad (priv, pad, TRUE); + if (priv->callbacks->demux_sent_eos) { + priv->callbacks->demux_sent_eos (&priv->engine, stream, priv->user_data); + } + GST_TEST_UNLOCK (priv); + } + + return GST_PAD_PROBE_OK; +} + +/* callback called when demux creates a src pad. + * We will create an AppSink to get the data + */ +static void +on_demuxNewPad (GstElement * demux, GstPad * pad, gpointer user_data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) user_data; + GstElement *pipeline; + GstElement *sink; + gboolean ret; + gchar *name; + GstPad *internal_pad; + GstAppSinkCallbacks appSinkCallbacks; + GstAdaptiveDemuxTestOutputStream *stream; + GObjectClass *gobject_class; + + fail_unless (priv != NULL); + name = gst_pad_get_name (pad); + + stream = g_slice_new0 (GstAdaptiveDemuxTestOutputStream); + GST_DEBUG ("created pad %p", pad); + + sink = gst_element_factory_make ("appsink", name); + g_free (name); + fail_unless (sink != NULL); + + GST_TEST_LOCK (priv); + + /* register the AppSink pointer in the test output data */ + gst_object_ref (sink); + stream->appsink = GST_APP_SINK (sink); + + appSinkCallbacks.eos = on_appSinkEOS; + appSinkCallbacks.new_preroll = NULL; + appSinkCallbacks.new_sample = on_appSinkNewSample; + + gst_app_sink_set_callbacks (GST_APP_SINK (sink), &appSinkCallbacks, priv, + NULL); + + gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_BUFFER, + (GstPadProbeCallback) on_demux_sent_data, priv, NULL); + gobject_class = G_OBJECT_GET_CLASS (sink); + if (g_object_class_find_property (gobject_class, "sync")) { + GST_DEBUG ("Setting sync=FALSE on AppSink"); + g_object_set (G_OBJECT (sink), "sync", FALSE, NULL); + } + stream->pad = gst_object_ref (pad); + internal_pad = + GST_PAD_CAST (gst_proxy_pad_get_internal (GST_PROXY_PAD (pad))); + + gst_pad_add_probe (internal_pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, + (GstPadProbeCallback) on_demuxReceivesEvent, priv, NULL); + + /* keep the reference to the internal_pad. + * We will need it to identify the stream in the on_demuxReceivesEvent callback + */ + stream->internal_pad = internal_pad; + + g_ptr_array_add (priv->engine.output_streams, stream); + GST_TEST_UNLOCK (priv); + + pipeline = GST_ELEMENT (gst_element_get_parent (demux)); + fail_unless (pipeline != NULL); + ret = gst_bin_add (GST_BIN (pipeline), sink); + fail_unless_equals_int (ret, TRUE); + gst_object_unref (pipeline); + ret = gst_element_link (demux, sink); + fail_unless_equals_int (ret, TRUE); + ret = gst_element_sync_state_with_parent (sink); + fail_unless_equals_int (ret, TRUE); + GST_TEST_LOCK (priv); + if (priv->callbacks->demux_pad_added) { + priv->callbacks->demux_pad_added (&priv->engine, stream, priv->user_data); + } + GST_TEST_UNLOCK (priv); +} + +/* callback called when demux removes a src pad. + * We remove the AppSink associated with this pad + */ +static void +on_demuxPadRemoved (GstElement * demux, GstPad * pad, gpointer user_data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) user_data; + GstAdaptiveDemuxTestOutputStream *stream = NULL; + + fail_unless (priv != NULL); + + GST_DEBUG ("Pad removed: %" GST_PTR_FORMAT, pad); + + GST_TEST_LOCK (priv); + stream = getTestOutputDataByPad (priv, pad, FALSE); + if (stream) { + GstStateChangeReturn ret; + GstState currentState, pending; + GstElement *appSink; + + if (priv->callbacks->demux_pad_removed) { + priv->callbacks->demux_pad_removed (&priv->engine, stream, + priv->user_data); + } + fail_unless (stream->appsink != NULL); + fail_unless (stream->internal_pad != NULL); + gst_object_unref (stream->internal_pad); + stream->internal_pad = NULL; + appSink = GST_ELEMENT (stream->appsink); + ret = gst_element_get_state (appSink, ¤tState, &pending, 0); + if ((ret == GST_STATE_CHANGE_SUCCESS && currentState == GST_STATE_PLAYING) + || (ret == GST_STATE_CHANGE_ASYNC && pending == GST_STATE_PLAYING)) { + GST_DEBUG ("Changing AppSink element to PAUSED"); + gst_element_set_state (appSink, GST_STATE_PAUSED); + } + } + GST_TEST_UNLOCK (priv); +} + +/* callback called when main_loop detects an error message + * We will signal main loop to quit + */ +static void +on_ErrorMessageOnBus (GstBus * bus, GstMessage * msg, gpointer user_data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) user_data; + GError *err = NULL; + gchar *dbg_info = NULL; + + gst_message_parse_error (msg, &err, &dbg_info); + GST_DEBUG ("ERROR from element %s: '%s'. Debugging info: %s", + GST_OBJECT_NAME (msg->src), err->message, (dbg_info) ? dbg_info : "none"); + g_error_free (err); + g_free (dbg_info); + + GST_TEST_LOCK (priv); + + fail_unless (priv->callbacks->bus_error_message, + "unexpected error detected on bus"); + + priv->callbacks->bus_error_message (&priv->engine, msg, priv->user_data); + + GST_TEST_UNLOCK (priv); +} + +static gboolean +start_pipeline_playing (gpointer user_data) +{ + GstAdaptiveDemuxTestEnginePrivate *priv = + (GstAdaptiveDemuxTestEnginePrivate *) user_data; + GstStateChangeReturn stateChange; + + GST_DEBUG ("Moving pipeline to PLAYING state"); + stateChange = + gst_element_set_state (priv->engine.pipeline, GST_STATE_PLAYING); + fail_unless (stateChange != GST_STATE_CHANGE_FAILURE); + GST_DEBUG ("PLAYING stateChange = %d", stateChange); + return FALSE; +} + +/* + * Create a demux element, run a test using the input data and check + * the output data + */ +void +gst_adaptive_demux_test_run (const gchar * element_name, + const gchar * manifest_uri, + const GstAdaptiveDemuxTestCallbacks * callbacks, gpointer user_data) +{ + GstBus *bus; + GstElement *demux; + GstElement *manifest_source; + gboolean ret; + GstStateChangeReturn stateChange; + GstAdaptiveDemuxTestEnginePrivate *priv; + + priv = g_slice_new0 (GstAdaptiveDemuxTestEnginePrivate); + priv->engine.output_streams = + g_ptr_array_new_with_free_func + (adaptive_demux_engine_stream_state_finalize); + g_mutex_init (&priv->engine.lock); + priv->callbacks = callbacks; + priv->user_data = user_data; + priv->engine.loop = g_main_loop_new (NULL, TRUE); + fail_unless (priv->engine.loop != NULL); + GST_TEST_LOCK (priv); + priv->engine.pipeline = gst_pipeline_new ("pipeline"); + fail_unless (priv->engine.pipeline != NULL); + GST_DEBUG ("created pipeline %" GST_PTR_FORMAT, priv->engine.pipeline); + + /* register a callback to listen for error messages */ + bus = gst_pipeline_get_bus (GST_PIPELINE (priv->engine.pipeline)); + gst_bus_add_signal_watch_full (bus, G_PRIORITY_HIGH); + g_signal_connect (bus, "message::error", + G_CALLBACK (on_ErrorMessageOnBus), priv); + + manifest_source = + gst_element_make_from_uri (GST_URI_SRC, manifest_uri, NULL, NULL); + fail_unless (manifest_source != NULL); + priv->engine.manifest_source = manifest_source; + + demux = gst_check_setup_element (element_name); + fail_unless (demux != NULL); + priv->engine.demux = demux; + GST_DEBUG ("created demux %" GST_PTR_FORMAT, demux); + + g_signal_connect (demux, "pad-added", G_CALLBACK (on_demuxNewPad), priv); + g_signal_connect (demux, "pad-removed", + G_CALLBACK (on_demuxPadRemoved), priv); + + gst_bin_add_many (GST_BIN (priv->engine.pipeline), manifest_source, demux, + NULL); + ASSERT_OBJECT_REFCOUNT (manifest_source, element_name, 1); + ASSERT_OBJECT_REFCOUNT (demux, element_name, 1); + + ret = gst_element_link (manifest_source, demux); + fail_unless_equals_int (ret, TRUE); + + /* call a test callback before we start the pipeline */ + if (callbacks->pre_test) + (*callbacks->pre_test) (&priv->engine, priv->user_data); + + GST_TEST_UNLOCK (priv); + + GST_DEBUG ("Starting pipeline"); + stateChange = gst_element_set_state (priv->engine.pipeline, GST_STATE_PAUSED); + fail_unless (stateChange != GST_STATE_CHANGE_FAILURE); + /* wait for completion of the move to PAUSED */ + stateChange = gst_element_get_state (priv->engine.pipeline, NULL, NULL, + GST_CLOCK_TIME_NONE); + fail_unless (stateChange != GST_STATE_CHANGE_FAILURE); + + g_idle_add ((GSourceFunc) start_pipeline_playing, priv); + + /* block until a callback calls g_main_loop_quit (engine.loop) */ + GST_DEBUG ("main thread waiting for streams to finish"); + g_main_loop_run (priv->engine.loop); + GST_DEBUG ("main thread finished. Stopping pipeline"); + + /* no need to use gst_element_get_state as the move the GST_STATE_NULL + is always synchronous */ + stateChange = gst_element_set_state (priv->engine.pipeline, GST_STATE_NULL); + fail_unless (stateChange != GST_STATE_CHANGE_FAILURE); + + GST_TEST_LOCK (priv); + + /* call a test callback after the stop of the pipeline */ + if (callbacks->post_test) + (*callbacks->post_test) (&priv->engine, priv->user_data); + + g_signal_handlers_disconnect_by_func (bus, + G_CALLBACK (on_ErrorMessageOnBus), priv); + gst_bus_remove_signal_watch (bus); + g_signal_handlers_disconnect_by_func (demux, G_CALLBACK (on_demuxNewPad), + priv); + g_signal_handlers_disconnect_by_func (demux, G_CALLBACK (on_demuxPadRemoved), + priv); + + GST_DEBUG ("main thread pipeline stopped"); + gst_object_unref (priv->engine.pipeline); + priv->engine.pipeline = NULL; + g_main_loop_unref (priv->engine.loop); + g_ptr_array_unref (priv->engine.output_streams); + + GST_TEST_UNLOCK (priv); + g_mutex_clear (&priv->engine.lock); + g_slice_free (GstAdaptiveDemuxTestEnginePrivate, priv); +} diff --git a/tests/check/elements/adaptive_demux_engine.h b/tests/check/elements/adaptive_demux_engine.h new file mode 100644 index 0000000000..267d7e8600 --- /dev/null +++ b/tests/check/elements/adaptive_demux_engine.h @@ -0,0 +1,185 @@ +/* A generic test engine for elements based upon GstAdaptiveDemux + * + * Copyright (c) <2015> YouView TV Ltd + * + * 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. + */ + +#ifndef __GST_ADAPTIVE_DEMUX_TEST_ENGINE_H__ +#define __GST_ADAPTIVE_DEMUX_TEST_ENGINE_H__ + +#include +#include +#include "test_http_src.h" + +G_BEGIN_DECLS + +typedef struct _GstAdaptiveDemuxTestEngine GstAdaptiveDemuxTestEngine; + +typedef struct _GstAdaptiveDemuxTestOutputStream { + /* the GstAppSink element getting the data for this stream */ + GstAppSink *appsink; + GstPad *pad; + /* the internal pad of adaptivedemux element used to send data to the GstAppSink element */ + GstPad *internal_pad; + /* current segment start offset */ + guint64 segment_start; + /* the size received so far on this segment */ + guint64 segment_received_size; + /* the total size received so far on this stream, excluding current segment */ + guint64 total_received_size; +} GstAdaptiveDemuxTestOutputStream; + +/* GstAdaptiveDemuxTestCallbacks: contains various callbacks that can + * be registered by a test. Not all callbacks needs to be configured + * by a test. A callback that is not required by a test must be set + * to NULL. + */ +typedef struct _GstAdaptiveDemuxTestCallbacks +{ + /** + * pre_test: called before starting the pipeline + * @engine: #GstAdaptiveDemuxTestEngine + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + */ + void (*pre_test) (GstAdaptiveDemuxTestEngine *engine, gpointer user_data); + + /** + * post_test: called after stopping the pipeline. + * @engine: #GstAdaptiveDemuxTestEngine + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + */ + void (*post_test) (GstAdaptiveDemuxTestEngine *engine, gpointer user_data); + + /** + * appsink_received_data: called each time AppSink receives data + * @engine: #GstAdaptiveDemuxTestEngine + * @stream: #GstAdaptiveDemuxTestOutputStream + * @buffer: the #GstBuffer that was recevied by #GstAppSink + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + * Returns: #TRUE to continue processing, #FALSE to cause EOS + * + * Can be used by a test to perform additional operations (eg validate + * output data) + */ + gboolean (*appsink_received_data) (GstAdaptiveDemuxTestEngine *engine, + GstAdaptiveDemuxTestOutputStream * stream, + GstBuffer * buffer, gpointer user_data); + + /** + * appsink_eos: called each time AppSink receives eos + * @engine: #GstAdaptiveDemuxTestEngine + * @stream: #GstAdaptiveDemuxTestOutputStream + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + * + * Can be used by a test to perform additional operations (eg validate + * output data) + */ + void (*appsink_eos) (GstAdaptiveDemuxTestEngine *engine, + GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data); + + /** + * demux_pad_added: called each time the demux creates a new pad + * @engine: #GstAdaptiveDemuxTestEngine + * @stream: the #GstAdaptiveDemuxTestOutputStream that has been created + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + */ + void (*demux_pad_added) (GstAdaptiveDemuxTestEngine * engine, + GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data); + + /** + * demux_pad_removed: called each time the demux removes a pad + * @engine: #GstAdaptiveDemuxTestEngine + * @stream: the #GstAdaptiveDemuxTestOutputStream that will no longer + * be used + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + */ + void (*demux_pad_removed) (GstAdaptiveDemuxTestEngine * engine, + GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data); + + /** + * demux_sent_data: called each time the demux sends data to AppSink + * @engine: #GstAdaptiveDemuxTestEngine + * @stream: #GstAdaptiveDemuxTestOutputStream + * @buffer: the #GstBuffer that was sent by demux + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + */ + gboolean (*demux_sent_data) (GstAdaptiveDemuxTestEngine *engine, + GstAdaptiveDemuxTestOutputStream * stream, + GstBuffer * buffer, gpointer user_data); + + /** + * demux_sent_eos: called each time demux send an EOS event + * @engine: #GstAdaptiveDemuxTestEngine + * @stream: #GstAdaptiveDemuxTestOutputStream + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + * Can be used by a test to perform additional operations (eg validate + * output data) + */ + void (*demux_sent_eos) (GstAdaptiveDemuxTestEngine *engine, + GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data); + + /** + * bus_error_message: called if an error is posted to the bus + * @engine: #GstAdaptiveDemuxTestEngine + * @msg: the #GstMessage that contains the error + * @user_data: the user_data passed to gst_adaptive_demux_test_run() + * + * The callback can decide if this error is expected, or to fail + * the test + */ + void (*bus_error_message)(GstAdaptiveDemuxTestEngine *engine, + GstMessage * msg, gpointer user_data); +} GstAdaptiveDemuxTestCallbacks; + +/* structure containing all data used by a test + * Any callback defined by a test will receive this as first parameter + */ +struct _GstAdaptiveDemuxTestEngine +{ + GstElement *pipeline; + GstElement *demux; + GstElement *manifest_source; + GMainLoop *loop; + GPtrArray *output_streams; /* GPtrArray */ + /* mutex to lock accesses to this structure when data is shared + * between threads */ + GMutex lock; +}; + +/** + * gst_adaptive_demux_test_run: + * @element_name: The name of the demux element (e.g. "dashdemux") + * @manifest_uri: The URI of the manifest to load + * @callbacks: The callbacks to use while the test is in operating + * @user_data: Opaque pointer that is passed to every callback + * + * Creates a pipeline with the specified demux element in it, + * connect a testhttpsrc element to this demux element and + * request manifest_uri. When the demux element adds a new + * pad, the engine will create an AppSink element and attach + * it to this pad. + * + * Information about these pads is collected in + * GstAdaptiveDemuxTestEngine::output_streams + */ +void gst_adaptive_demux_test_run (const gchar * element_name, + const gchar * manifest_uri, + const GstAdaptiveDemuxTestCallbacks * callbacks, + gpointer user_data); + +G_END_DECLS +#endif /* __GST_ADAPTIVE_DEMUX_TEST_ENGINE_H__ */ diff --git a/tests/check/elements/dash_demux.c b/tests/check/elements/dash_demux.c index 55f5c8cdad..ce7556fb7b 100644 --- a/tests/check/elements/dash_demux.c +++ b/tests/check/elements/dash_demux.c @@ -19,660 +19,80 @@ */ #include -#include -#include "fake_http_src.h" +#include "adaptive_demux_common.h" -#define GST_FAKE_SOUP_HTTP_SRC_NAME "fake-soup-http-src" +#define DEMUX_ELEMENT_NAME "dashdemux" -/* forward declarations */ -struct _GstDashDemuxTestData; +#define COPY_OUTPUT_TEST_DATA(outputTestData,testData) do { \ + guint otdPos, otdLen = sizeof((outputTestData)) / sizeof((outputTestData)[0]); \ + for(otdPos=0; otdPosoutput_streams = g_list_append ((testData)->output_streams, &(outputTestData)[otdPos]); \ + } \ + } while(0) -/* the seek test will use a separate task to perform the seek operation. - * After starting the task, the caller will block until the seek task - * flushes the AppSink and changes the GstFakeSoupHTTPSrc element state from - * PLAYING to PAUSED. - * When that event is detected, the caller is allowed to resume. - * Any data that will be sent to AppSink after resume will be rejected because - * AppSink is in flushing mode. - */ -typedef enum +typedef struct _GstDashDemuxTestInputData { - SEEK_TASK_STATE_NOT_STARTED, - SEEK_TASK_STATE_WAITING_FOR_FAKESRC_STATE_CHANGE, - SEEK_TASK_STATE_EXITING, -} SeekTaskState; + const gchar *uri; + const guint8 *payload; + guint64 size; +} GstDashDemuxTestInputData; -/* scratchData space used by tests to store output stream related data. - * Structure is set to 0 when the test begins. - * If a test does not use some of the fields, they will remain 0. - * As new tests will want new functionality, this structure can gain new - * members without affecting existing tests. - */ -typedef struct _GstDashDemuxTestOutputStreamScratchData +static gboolean +gst_dashdemux_http_src_start (GstTestHTTPSrc * src, + const gchar * uri, GstTestHTTPSrcInput * input_data, gpointer user_data) { - /* the GstAppSink element getting the data for this stream */ - GstAppSink *appsink; - /* the internal pad of adaptivedemux element used to send data to the GstAppSink element */ - GstPad *internalPad; - /* current segment start offset */ - guint64 segmentStart; - /* the size received so far on this segment */ - guint64 segmentReceivedSize; - /* the total size received so far on this stream, excluding current segment */ - guint64 totalReceivedSize; -} GstDashDemuxTestOutputStreamScratchData; - -/* structure to store output stream related data. - * Will be used by the test during output validation. - * The fields are set by the user in the test using static arrays. - * The scratchData field is set to a NULL pointer and later allocated by the - * test and initialised with 0. - */ -typedef struct _GstDashDemuxTestOutputStreamData -{ - /* the name of the dashdemux src pad generating this stream */ - const char *name; - /* the expected size on this stream */ - guint expectedSize; - /* stream level scratchData space to be used by test */ - GstDashDemuxTestOutputStreamScratchData *scratchData; -} GstDashDemuxTestOutputStreamData; - -/* callback that will be called before starting the pipeline. - */ -typedef void (*PreTestCallback) (struct _GstDashDemuxTestData * testData); - -/* callback that will be called after stopping the pipeline. - */ -typedef void (*PostTestCallback) (struct _GstDashDemuxTestData * testData); - -/* callback that will be called each time AppSink received data. - * Can be used by a test to perform additional operations (eg validate - * output data) - */ -typedef gboolean (*AppSinkGotDataCallback) (struct _GstDashDemuxTestData * - testData, GstDashDemuxTestOutputStreamData * testOutputStreamData, - GstBuffer * buffer); - -/* callback that will be called each time AppSink received eos. - * Can be used by a test to perform additional operations (eg validate - * output data) - */ -typedef gboolean (*AppSinkGotEosCallback) (struct _GstDashDemuxTestData * - testData, GstDashDemuxTestOutputStreamData * testOutputStreamData); - -/* callback that will be called each time dashdemux sends data to AppSink - */ -typedef gboolean (*DashdemuxSendsDataCallback) (struct _GstDashDemuxTestData - * testData, GstDashDemuxTestOutputStreamData * testOutputStreamData, - GstBuffer * buffer); - -/* structure containing various callbacks that can be registered by a test - * Not all callbacks needs to be configured by a test. - * As new tests will want new functionality, this structure can gain new - * members without affecting existing tests. - */ -typedef struct -{ - PreTestCallback preTestCallback; - PostTestCallback postTestCallback; - AppSinkGotDataCallback appSinkGotDataCallback; - AppSinkGotEosCallback appSinkGotEosCallback; - DashdemuxSendsDataCallback dashdemuxSendsDataCallback; -} Callbacks; - -/* scratchData space used by tests to store data. - * Structure is set to 0 when the test begins. - * If a test does not use some of the fields, they will remain 0. - * As new tests will want new functionality, this structure can gain new - * members without affecting existing tests. - */ -typedef struct _GstDashDemuxTestScratchData -{ - GstElement *pipeline; - GstElement *dashdemux; - GstElement *mpd_source; - GMainLoop *loop; - - /* task used by the seek test to perform the seek operation */ - GstTask *seekTask; - GRecMutex seekTaskLock; - - /* the state of the seekTask */ - SeekTaskState seekTaskState; - GMutex seekTaskStateLock; - GCond seekTaskStateCond; - - /* seek test will wait for this amount of bytes to be sent by dash to AppSink - * before triggering a seek request - */ - guint thresholdForSeek; - -} GstDashDemuxTestScratchData; - -/* structure containing all data used by a test - * Any callback defined by a test will receive this as first parameter - */ -typedef struct _GstDashDemuxTestData -{ - /* input data to configure the GstFakeSoupHTTPSrc element - * Null terminaled array of GstFakeHttpSrcInputData, one entry per stream - */ - const GstFakeHttpSrcInputData *inputStreamArray; - - /* output data used to validate the test - * array of GstDashDemuxTestOutputStreamData, one entry per stream - */ - GstDashDemuxTestOutputStreamData *outputStreamArray; - guint outputStreamArraySize; - - /* test level scratchData data to be used by test */ - GstDashDemuxTestScratchData *scratchData; - - /* mutex to lock accesses to this structure when data is shared between threads */ - GMutex lockTestData; - - /* the number of streams that finished. - * Main thread will stop the pipeline when all streams are finished - * (countStreamFinished == outputStreamArraySize) - */ - guint countStreamFinished; - - /* true if an error is expected on pipeline - * If this is not set and an error is received, the test is failed - */ - gboolean expectError; - - /* callbacks to be registered by test to influence the pipeline */ - const Callbacks *callbacks; - -} GstDashDemuxTestData; - -#define GST_TEST_GET_LOCK(d) (&(((GstDashDemuxTestData*)(d))->lockTestData)) -#define GST_TEST_LOCK(d) g_mutex_lock (GST_TEST_GET_LOCK (d)) -#define GST_TEST_UNLOCK(d) g_mutex_unlock (GST_TEST_GET_LOCK (d)) - -/* global pointer to test data, set before each test. - * We need it global so that gst_fake_soup_http_src element can get the data. - * Due to this, tests cannot be run in parallel. -*/ -static GstDashDemuxTestData *g_testData = NULL; - -static void -test_setup (void) -{ - GstRegistry *registry; - gboolean ret; - - registry = gst_registry_get (); - ret = - gst_fake_soup_http_src_register_plugin (registry, - GST_FAKE_SOUP_HTTP_SRC_NAME); - fail_unless (ret); -} - -static void -test_teardown (void) -{ -} - -/* get the testOutput entry in testData corresponding to the given AppSink */ -static GstDashDemuxTestOutputStreamData * -getTestOutputDataByAppsink (GstDashDemuxTestData * testData, - GstAppSink * appsink) -{ - for (guint i = 0; i < testData->outputStreamArraySize; ++i) { - if (testData->outputStreamArray[i].scratchData->appsink == appsink) { - return &testData->outputStreamArray[i]; + const GstDashDemuxTestInputData *input = + (const GstDashDemuxTestInputData *) user_data; + for (guint i = 0; input[i].uri; ++i) { + if (strcmp (input[i].uri, uri) == 0) { + input_data->context = (gpointer) & input[i]; + input_data->size = input[i].size; + if (input[i].size == 0) + input_data->size = strlen ((gchar *) input[i].payload); + return TRUE; } } - ck_abort_msg ("cannot find appsink %p in the output data", appsink); - return NULL; + return FALSE; } -/* get the testOutput entry in testData corresponding to the given stream name */ -static GstDashDemuxTestOutputStreamData * -getTestOutputDataByName (GstDashDemuxTestData * testData, const char *name) -{ - for (guint i = 0; i < testData->outputStreamArraySize; ++i) { - if (strcmp (testData->outputStreamArray[i].name, name) == 0) { - return &testData->outputStreamArray[i]; - } - } - ck_abort_msg ("cannot find stream '%s' in the output data", name); - return NULL; -} - -/* get the testOutput entry in testData corresponding to the given internal pad */ -static GstDashDemuxTestOutputStreamData * -getTestOutputDataByPad (GstDashDemuxTestData * testData, const GstPad * pad) -{ - for (guint i = 0; i < testData->outputStreamArraySize; ++i) { - if (testData->outputStreamArray[i].scratchData->internalPad == pad) { - return &testData->outputStreamArray[i]; - } - } - ck_abort_msg ("cannot find pad '%p' in the output data", pad); - return NULL; -} - -/* callback called when AppSink receives data */ static GstFlowReturn -on_appSinkNewSample (GstAppSink * appsink, gpointer user_data) +gst_dashdemux_http_src_create (GstTestHTTPSrc * src, + guint64 offset, + guint length, GstBuffer ** retbuf, gpointer context, gpointer user_data) { - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) user_data; - GstDashDemuxTestOutputStreamData *testOutputStreamData = NULL; - GstSample *sample; - GstBuffer *buffer; - gboolean ret = TRUE; + /* const GstDashDemuxTestInputData *input = + (const GstDashDemuxTestInputData *) user_data; */ + const GstDashDemuxTestInputData *input = + (const GstDashDemuxTestInputData *) context; + GstBuffer *buf; - fail_unless (testData != NULL); + buf = gst_buffer_new_allocate (NULL, length, NULL); + fail_if (buf == NULL, "Not enough memory to allocate buffer"); - GST_TEST_LOCK (testData); + if (input->payload) { + gst_buffer_fill (buf, 0, input->payload + offset, length); + } else { + GstMapInfo info; + guint pattern; - testOutputStreamData = getTestOutputDataByAppsink (testData, appsink); - - sample = gst_app_sink_pull_sample (appsink); - buffer = gst_sample_get_buffer (sample); - - /* call the test callback, if registered */ - if (testData->callbacks->appSinkGotDataCallback) - ret = testData->callbacks->appSinkGotDataCallback (testData, - testOutputStreamData, buffer); - - testOutputStreamData->scratchData->segmentReceivedSize += - gst_buffer_get_size (buffer); - - gst_sample_unref (sample); - - GST_TEST_UNLOCK (testData); - - if (!ret) - return GST_FLOW_EOS; + pattern = offset - offset % sizeof (pattern); + gst_buffer_map (buf, &info, GST_MAP_WRITE); + for (guint64 i = 0; i < length; ++i) { + gchar pattern_byte_to_write = (offset + i) % sizeof (pattern); + if (pattern_byte_to_write == 0) { + pattern = offset + i; + } + info.data[i] = (pattern >> (pattern_byte_to_write * 8)) & 0xFF; + } + gst_buffer_unmap (buf, &info); + } + *retbuf = buf; return GST_FLOW_OK; } -/* callback called when AppSink receives eos */ -static void -on_appSinkEOS (GstAppSink * appsink, gpointer user_data) -{ - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) user_data; - GstDashDemuxTestOutputStreamData *testOutputStreamData = NULL; - gboolean ret = TRUE; - - fail_unless (testData != NULL); - - GST_TEST_LOCK (testData); - - testOutputStreamData = getTestOutputDataByAppsink (testData, appsink); - - testOutputStreamData->scratchData->totalReceivedSize += - testOutputStreamData->scratchData->segmentReceivedSize; - testOutputStreamData->scratchData->segmentReceivedSize = 0; - - if (testData->callbacks->appSinkGotEosCallback) - ret = testData->callbacks->appSinkGotEosCallback (testData, - testOutputStreamData); - - if (ret) { - /* signal to the application that another stream has finished */ - testData->countStreamFinished++; - - if (testData->countStreamFinished == testData->outputStreamArraySize) { - g_main_loop_quit (testData->scratchData->loop); - } - - } else { - /* ignore this eos event, the stream is not finished yet */ - } - - GST_TEST_UNLOCK (testData); -} - -/* callback called when dash sends data to AppSink */ -static GstPadProbeReturn -on_dashSendsData (GstPad * pad, GstPadProbeInfo * info, gpointer data) -{ - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) data; - GstDashDemuxTestOutputStreamData *testOutputStreamData = NULL; - GstBuffer *buffer; - char *streamName; - - buffer = GST_PAD_PROBE_INFO_BUFFER (info); - - GST_TEST_LOCK (testData); - - streamName = gst_pad_get_name (pad); - testOutputStreamData = getTestOutputDataByName (testData, streamName); - g_free (streamName); - - if (testData->callbacks->dashdemuxSendsDataCallback) { - (*testData->callbacks->dashdemuxSendsDataCallback) (testData, - testOutputStreamData, buffer); - } - - GST_TEST_UNLOCK (testData); - - return GST_PAD_PROBE_OK; -} - -/* callback called when dash receives events from GstFakeSoupHTTPSrc */ -static GstPadProbeReturn -on_dashReceivesEvent (GstPad * pad, GstPadProbeInfo * info, gpointer data) -{ - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) data; - GstDashDemuxTestOutputStreamData *testOutputStreamData = NULL; - GstEvent *event; - const GstSegment *segment; - - event = GST_PAD_PROBE_INFO_EVENT (info); - GST_DEBUG ("dash received event %" GST_PTR_FORMAT " on pad %p", event, pad); - - if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) { - /* a new segment will start arriving - * update segmentStart used by pattern validation - */ - gst_event_parse_segment (event, &segment); - - GST_TEST_LOCK (testData); - - testOutputStreamData = getTestOutputDataByPad (testData, pad); - - testOutputStreamData->scratchData->totalReceivedSize += - testOutputStreamData->scratchData->segmentReceivedSize; - testOutputStreamData->scratchData->segmentReceivedSize = 0; - testOutputStreamData->scratchData->segmentStart = segment->start; - - GST_TEST_UNLOCK (testData); - } - - return GST_PAD_PROBE_OK; -} - -/* callback called when dashdemux creates a src pad. - * We will create an AppSink to get the data - */ -static void -on_dashNewPad (GstElement * dashdemux, GstPad * pad, gpointer data) -{ - GstElement *pipeline; - GstElement *sink; - gboolean ret; - char *name; - GstPad *internalPad; - GstAppSinkCallbacks appSinkCallbacks; - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) data; - GstDashDemuxTestOutputStreamData *testOutputStreamData = NULL; - - fail_unless (testData != NULL); - name = gst_pad_get_name (pad); - - GST_DEBUG ("created pad %p", pad); - - sink = gst_element_factory_make ("appsink", name); - fail_unless (sink != NULL); - - GST_TEST_LOCK (testData); - - testOutputStreamData = getTestOutputDataByName (testData, name); - g_free (name); - - /* register the AppSink pointer in the test output data */ - gst_object_ref (sink); - testOutputStreamData->scratchData->appsink = GST_APP_SINK (sink); - - appSinkCallbacks.eos = on_appSinkEOS; - appSinkCallbacks.new_preroll = NULL; - appSinkCallbacks.new_sample = on_appSinkNewSample; - - gst_app_sink_set_callbacks (GST_APP_SINK (sink), &appSinkCallbacks, testData, - NULL); - - gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_BUFFER, - (GstPadProbeCallback) on_dashSendsData, testData, NULL); - - internalPad = GST_PAD_CAST (gst_proxy_pad_get_internal (GST_PROXY_PAD (pad))); - - gst_pad_add_probe (internalPad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, - (GstPadProbeCallback) on_dashReceivesEvent, testData, NULL); - - /* keep the reference to the internalPad. - * We will need it to identify the stream in the on_dashReceivesEvent callback - */ - testOutputStreamData->scratchData->internalPad = internalPad; - - GST_TEST_UNLOCK (testData); - - pipeline = GST_ELEMENT (gst_element_get_parent (dashdemux)); - fail_unless (pipeline != NULL); - ret = gst_bin_add (GST_BIN (pipeline), sink); - fail_unless_equals_int (ret, TRUE); - gst_object_unref (pipeline); - ret = gst_element_link (dashdemux, sink); - fail_unless_equals_int (ret, TRUE); - ret = gst_element_sync_state_with_parent (sink); - fail_unless_equals_int (ret, TRUE); - -} - -/* callback called when main_loop detects an error message - * We will signal main loop to quit - */ -static void -on_ErrorMessageOnBus (GstBus * bus, GstMessage * msg, gpointer data) -{ - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) data; - GError *err = NULL; - gchar *dbg_info = NULL; - - gst_message_parse_error (msg, &err, &dbg_info); - GST_DEBUG ("ERROR from element %s: '%s'. Debugging info: %s", - GST_OBJECT_NAME (msg->src), err->message, (dbg_info) ? dbg_info : "none"); - g_error_free (err); - g_free (dbg_info); - - GST_TEST_LOCK (testData); - - fail_unless (testData->expectError == TRUE, - "unexpected error detected on bus"); - - g_main_loop_quit (testData->scratchData->loop); - - GST_TEST_UNLOCK (testData); -} - -/* - * Create a dashdemux element, run a test using the input data and check - * the output data - */ -static void -runTest (const GstFakeHttpSrcInputData * inputStreamArray, - GstDashDemuxTestOutputStreamData * outputStreamArray, - guint outputStreamArraySize, const Callbacks * callbacks) -{ - GstElement *pipeline; - GstBus *bus; - GstElement *dashdemux; - GstElement *mpd_source; - gboolean ret; - GstStateChangeReturn stateChange; - GstDashDemuxTestData *testData; - - testData = g_slice_new0 (GstDashDemuxTestData); - testData->inputStreamArray = inputStreamArray; - testData->outputStreamArray = outputStreamArray; - testData->outputStreamArraySize = outputStreamArraySize; - g_mutex_init (&testData->lockTestData); - testData->callbacks = callbacks; - - /* allocate the scratchData space */ - testData->scratchData = g_slice_new0 (GstDashDemuxTestScratchData); - for (guint i = 0; i < outputStreamArraySize; ++i) { - outputStreamArray[i].scratchData = - g_slice_new0 (GstDashDemuxTestOutputStreamScratchData); - } - - fail_unless (g_testData == NULL); - g_testData = testData; - gst_fake_soup_http_src_set_input_data (inputStreamArray); - - testData->scratchData->loop = g_main_loop_new (NULL, TRUE); - - GST_TEST_LOCK (testData); - - pipeline = gst_pipeline_new ("pipeline"); - fail_unless (pipeline != NULL); - testData->scratchData->pipeline = pipeline; - GST_DEBUG ("created pipeline %p", pipeline); - - /* register a callback to listen for error messages */ - bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline)); - gst_bus_add_signal_watch (bus); - g_signal_connect (bus, "message::error", - G_CALLBACK (on_ErrorMessageOnBus), testData); - - dashdemux = gst_check_setup_element ("dashdemux"); - fail_unless (dashdemux != NULL); - testData->scratchData->dashdemux = dashdemux; - GST_DEBUG ("created dash %p", dashdemux); - - g_signal_connect (dashdemux, "pad-added", G_CALLBACK (on_dashNewPad), - testData); - - ret = gst_bin_add (GST_BIN (pipeline), dashdemux); - fail_unless_equals_int (ret, TRUE); - - /* assume first entry in inputStreamArray configures the mpd stream */ - mpd_source = - gst_element_make_from_uri (GST_URI_SRC, inputStreamArray[0].uri, NULL, - NULL); - fail_unless (mpd_source != NULL); - testData->scratchData->mpd_source = mpd_source; - - ret = gst_bin_add (GST_BIN (pipeline), mpd_source); - fail_unless_equals_int (ret, TRUE); - - ret = gst_element_link (mpd_source, dashdemux); - fail_unless_equals_int (ret, TRUE); - - /* call a test callback before we start the pipeline */ - if (callbacks->preTestCallback) - (*callbacks->preTestCallback) (testData); - - GST_TEST_UNLOCK (testData); - - GST_DEBUG ("starting pipeline"); - gst_element_set_state (pipeline, GST_STATE_PLAYING); - stateChange = gst_element_get_state (pipeline, NULL, NULL, - GST_CLOCK_TIME_NONE); - fail_unless_equals_int (stateChange, GST_STATE_CHANGE_SUCCESS); - - /* block until all output streams received an eos event */ - GST_DEBUG ("main thread waiting for streams to finish"); - g_main_loop_run (testData->scratchData->loop); - GST_DEBUG ("main thread all streams finished. Stopping pipeline"); - - g_main_loop_unref (testData->scratchData->loop); - - stateChange = gst_element_set_state (pipeline, GST_STATE_NULL); - fail_unless_equals_int (stateChange, GST_STATE_CHANGE_SUCCESS); - GST_DEBUG ("main thread pipeline stopped"); - gst_object_unref (pipeline); - - GST_TEST_LOCK (testData); - - /* call a test callback after the stop of the pipeline */ - if (callbacks->postTestCallback) - (*callbacks->postTestCallback) (testData); - - GST_TEST_UNLOCK (testData); - - for (guint i = 0; i < outputStreamArraySize; ++i) { - if (outputStreamArray[i].scratchData->appsink) - gst_object_unref (outputStreamArray[i].scratchData->appsink); - - if (outputStreamArray[i].scratchData->internalPad) - gst_object_unref (outputStreamArray[i].scratchData->internalPad); - - g_slice_free (GstDashDemuxTestOutputStreamScratchData, - outputStreamArray[i].scratchData); - } - g_slice_free (GstDashDemuxTestScratchData, testData->scratchData); - - g_mutex_clear (&testData->lockTestData); - g_testData = NULL; - g_slice_free (GstDashDemuxTestData, testData); -} - -/******************** Dash demux test engine definition ends here *************/ - /******************** Test specific code starts here **************************/ -/* function to validate data received by AppSink */ -static gboolean -checkDataReceived (GstDashDemuxTestData * testData, - GstDashDemuxTestOutputStreamData * testOutputStreamData, GstBuffer * buffer) -{ - GstMapInfo info; - guint pattern; - guint64 streamOffset; - - gst_buffer_map (buffer, &info, GST_MAP_READ); - - GST_DEBUG - ("segmentStart = %" G_GUINT64_FORMAT " segmentReceivedSize = %" - G_GUINT64_FORMAT " bufferSize=%d", - testOutputStreamData->scratchData->segmentStart, - testOutputStreamData->scratchData->segmentReceivedSize, (gint) info.size); - - streamOffset = testOutputStreamData->scratchData->segmentStart + - testOutputStreamData->scratchData->segmentReceivedSize; - pattern = streamOffset - streamOffset % sizeof (pattern); - for (guint64 i = 0; i != info.size; ++i) { - guint received = info.data[i]; - guint expected; - gchar pattern_byte_to_read; - - pattern_byte_to_read = (streamOffset + i) % sizeof (pattern); - if (pattern_byte_to_read == 0) { - pattern = streamOffset + i; - } - expected = (pattern >> (pattern_byte_to_read * 8)) & 0xFF; - -/* - GST_DEBUG - ("received '0x%02x' expected '0x%02x' offset %" G_GUINT64_FORMAT - " pattern=%08x byte_to_read=%d", - received, expected, i, pattern, pattern_byte_to_read); -*/ - - fail_unless (received == expected, - "output validation failed: received '0x%02x' expected '0x%02x' offset %" - G_GUINT64_FORMAT " pattern=%08x byte_to_read=%d\n", received, expected, - i, pattern, pattern_byte_to_read); - } - - gst_buffer_unmap (buffer, &info); - return TRUE; -} - -/* function to check total size of data received by AppSink - * will be called when AppSink receives eos. - */ -static gboolean -checkSizeOfDataReceived (GstDashDemuxTestData * testData, - GstDashDemuxTestOutputStreamData * testOutputStreamData) -{ - fail_unless (testOutputStreamData->scratchData->totalReceivedSize == - testOutputStreamData->expectedSize, - "size validation failed for %s, expected %d received %d", - testOutputStreamData->name, testOutputStreamData->expectedSize, - testOutputStreamData->scratchData->totalReceivedSize); - - return TRUE; -} - /* * Test an mpd with an audio and a video stream * @@ -721,26 +141,34 @@ GST_START_TEST (simpleTest) " " " " " "; - - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, {"http://unit.test/audio.webm", NULL, 5000}, {"http://unit.test/video.webm", NULL, 9000}, {NULL, NULL, 0}, }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { {"audio_00", 5000, NULL}, - {"video_00", 9000, NULL}, + {"video_00", 9000, NULL} }; + GstAdaptiveDemuxTestCallbacks test_callbacks = { 0 }; + GstAdaptiveDemuxTestCase *testData; - Callbacks cb = { 0 }; - cb.appSinkGotDataCallback = checkDataReceived; - cb.appSinkGotEosCallback = checkSizeOfDataReceived; + testData = gst_adaptive_demux_test_case_new (); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = gst_dashdemux_http_src_create; + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); - runTest (inputTestData, - outputTestData, sizeof (outputTestData) / sizeof (outputTestData[0]), - &cb); + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + test_callbacks.appsink_received_data = + gst_adaptive_demux_test_check_received_data; + test_callbacks.appsink_eos = + gst_adaptive_demux_test_check_size_of_received_data; + + gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME, "http://unit.test/test.mpd", + &test_callbacks, testData); + g_object_unref (testData); } GST_END_TEST; @@ -828,30 +256,37 @@ GST_START_TEST (testTwoPeriods) " " " "; - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, {"http://unit.test/audio1.webm", NULL, 5001}, {"http://unit.test/video1.webm", NULL, 9001}, {"http://unit.test/audio2.webm", NULL, 5002}, {"http://unit.test/video2.webm", NULL, 9002}, {NULL, NULL, 0}, }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { {"audio_00", 5001, NULL}, {"video_00", 9001, NULL}, {"audio_01", 5002, NULL}, {"video_01", 9002, NULL}, }; + GstAdaptiveDemuxTestCallbacks test_callbacks = { 0 }; + GstAdaptiveDemuxTestCase *testData; - Callbacks cb = { 0 }; - cb.appSinkGotDataCallback = checkDataReceived; - cb.appSinkGotEosCallback = checkSizeOfDataReceived; - - runTest (inputTestData, - outputTestData, sizeof (outputTestData) / sizeof (outputTestData[0]), - &cb); + testData = gst_adaptive_demux_test_case_new (); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = gst_dashdemux_http_src_create; + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + test_callbacks.appsink_received_data = + gst_adaptive_demux_test_check_received_data; + test_callbacks.appsink_eos = + gst_adaptive_demux_test_check_size_of_received_data; + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); + gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME, + "http://unit.test/test.mpd", &test_callbacks, testData); + gst_object_unref (testData); } GST_END_TEST; @@ -909,24 +344,25 @@ do \ } while (0) static void -setAndTestDashParams (GstDashDemuxTestData * testData) +setAndTestDashParams (GstAdaptiveDemuxTestEngine * engine, gpointer user_data) { - GstElement *dashdemux = testData->scratchData->dashdemux; + /* GstAdaptiveDemuxTestCase * testData = (GstAdaptiveDemuxTestCase*)user_data; */ + GObject *dashdemux = G_OBJECT (engine->demux); - test_int_prop (G_OBJECT (dashdemux), "connection-speed", 1000); - test_invalid_int_prop (G_OBJECT (dashdemux), "connection-speed", 4294967 + 1); + test_int_prop (dashdemux, "connection-speed", 1000); + test_invalid_int_prop (dashdemux, "connection-speed", 4294967 + 1); - test_float_prop (G_OBJECT (dashdemux), "bitrate-limit", 1); - test_invalid_float_prop (G_OBJECT (dashdemux), "bitrate-limit", 2.1); + test_float_prop (dashdemux, "bitrate-limit", 1); + test_invalid_float_prop (dashdemux, "bitrate-limit", 2.1); - test_int_prop (G_OBJECT (dashdemux), "max-buffering-time", 15); - test_invalid_int_prop (G_OBJECT (dashdemux), "max-buffering-time", 1); + test_int_prop (dashdemux, "max-buffering-time", 15); + test_invalid_int_prop (dashdemux, "max-buffering-time", 1); - test_float_prop (G_OBJECT (dashdemux), "bandwidth-usage", 0.5); - test_invalid_float_prop (G_OBJECT (dashdemux), "bandwidth-usage", 2); + test_float_prop (dashdemux, "bandwidth-usage", 0.5); + test_invalid_float_prop (dashdemux, "bandwidth-usage", 2); - test_int_prop (G_OBJECT (dashdemux), "max-bitrate", 1000); - test_invalid_int_prop (G_OBJECT (dashdemux), "max-bitrate", 10); + test_int_prop (dashdemux, "max-bitrate", 1000); + test_invalid_int_prop (dashdemux, "max-bitrate", 10); } /* @@ -963,215 +399,36 @@ GST_START_TEST (testParameters) " " " "; - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, {"http://unit.test/audio.webm", NULL, 5000}, {NULL, NULL, 0}, }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { {"audio_00", 5000, NULL}, }; + GstAdaptiveDemuxTestCallbacks test_callbacks = { 0 }; + GstAdaptiveDemuxTestCase *testData; - Callbacks cb = { 0 }; - cb.preTestCallback = setAndTestDashParams; - cb.appSinkGotDataCallback = checkDataReceived; - cb.appSinkGotEosCallback = checkSizeOfDataReceived; + testData = gst_adaptive_demux_test_case_new (); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = gst_dashdemux_http_src_create; + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + test_callbacks.pre_test = setAndTestDashParams; + test_callbacks.appsink_received_data = + gst_adaptive_demux_test_check_received_data; + test_callbacks.appsink_eos = + gst_adaptive_demux_test_check_size_of_received_data; - runTest (inputTestData, - outputTestData, sizeof (outputTestData) / sizeof (outputTestData[0]), - &cb); + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); + gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME, "http://unit.test/test.mpd", + &test_callbacks, testData); + gst_object_unref (testData); } GST_END_TEST; -/* check data received by AppSink when eos is received */ -static gboolean -testSeekCheckSizeOfDataReceived (GstDashDemuxTestData * testData, - GstDashDemuxTestOutputStreamData * testOutputStreamData) -{ - guint64 expectedSize; - - GST_DEBUG ("checkDataReceivedTestSeek for %s received %" G_GUINT64_FORMAT, - testOutputStreamData->name, - testOutputStreamData->scratchData->totalReceivedSize); - - /* the seek was to the beginning of the file, so expect to receive - * thresholdForSeek + a whole file - */ - expectedSize = testData->scratchData->thresholdForSeek + - testOutputStreamData->expectedSize; - fail_unless (testOutputStreamData->scratchData->totalReceivedSize == - expectedSize, - "size validation failed for %s, expected %" G_GUINT64_FORMAT " received %" - G_GUINT64_FORMAT, testOutputStreamData->name, expectedSize, - testOutputStreamData->scratchData->totalReceivedSize); - - return TRUE; -} - -/* function to generate a seek event. Will be run in a separate thread */ -static void -testSeekTaskDoSeek (GstDashDemuxTestData * testData) -{ - GST_DEBUG ("testSeekTaskDoSeek calling seek"); - - /* seek to 5ms. - * Because there is only one fragment, we expect the whole file to be - * downloaded again - */ - if (!gst_element_seek_simple (GST_ELEMENT (testData->scratchData->pipeline), - GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, - 5 * GST_MSECOND)) { - fail ("Seek failed!\n"); - } - GST_DEBUG ("seek ok"); - gst_task_stop (testData->scratchData->seekTask); -} - -/* function to be called during seek test when dash sends data to AppSink - * It monitors the data sent and after a while will generate a seek request. - */ -static gboolean -testSeekDashdemuxSendsData (GstDashDemuxTestData * testData, - GstDashDemuxTestOutputStreamData * testOutputStreamData, GstBuffer * buffer) -{ - if (strcmp (testOutputStreamData->name, "audio_00") == 0 && - testData->scratchData->seekTask == NULL && - testOutputStreamData->scratchData->totalReceivedSize + - testOutputStreamData->scratchData->segmentReceivedSize >= - testData->scratchData->thresholdForSeek) { - - GST_DEBUG ("starting seek task"); - - g_mutex_lock (&testData->scratchData->seekTaskStateLock); - testData->scratchData->seekTaskState = - SEEK_TASK_STATE_WAITING_FOR_FAKESRC_STATE_CHANGE; - g_mutex_unlock (&testData->scratchData->seekTaskStateLock); - - g_rec_mutex_init (&testData->scratchData->seekTaskLock); - testData->scratchData->seekTask = - gst_task_new ((GstTaskFunction) testSeekTaskDoSeek, testData, NULL); - gst_task_set_lock (testData->scratchData->seekTask, - &testData->scratchData->seekTaskLock); - gst_task_start (testData->scratchData->seekTask); - - GST_DEBUG ("seek task started"); - - g_mutex_lock (&testData->scratchData->seekTaskStateLock); - - GST_DEBUG ("waiting for seek task to change state on fakesrc"); - - /* wait for seekTask to run, send a flush start event to AppSink - * and change the fakesouphttpsrc element state from PLAYING to PAUSED - */ - while (testData->scratchData->seekTaskState == - SEEK_TASK_STATE_WAITING_FOR_FAKESRC_STATE_CHANGE) { - g_cond_wait (&testData->scratchData->seekTaskStateCond, - &testData->scratchData->seekTaskStateLock); - } - g_mutex_unlock (&testData->scratchData->seekTaskStateLock); - /* we can continue now, but this buffer will be rejected by AppSink - * because it is in flushing mode - */ - GST_DEBUG ("seek task changed state on fakesrc, resuming"); - } - - return TRUE; -} - -/* callback called when main_loop detects a state changed event */ -static void -testSeekOnStateChanged (GstBus * bus, GstMessage * msg, gpointer data) -{ - GstDashDemuxTestData *testData = (GstDashDemuxTestData *) data; - GstDashDemuxTestOutputStreamData *testOutputStreamData; - GstState old_state, new_state; - const char *srcName = GST_OBJECT_NAME (msg->src); - GstPad *internalPad; - - gst_message_parse_state_changed (msg, &old_state, &new_state, NULL); - GST_DEBUG ("Element %s changed state from %s to %s", - GST_OBJECT_NAME (msg->src), - gst_element_state_get_name (old_state), - gst_element_state_get_name (new_state)); - - if (strstr (srcName, "fakesouphttpsrc") == srcName && - old_state == GST_STATE_PLAYING && new_state == GST_STATE_PAUSED) { - GList *pads = GST_ELEMENT_PADS (msg->src); - GstObject *srcBin; - - /* src is a fake http src element. It should have only 1 pad */ - fail_unless (pads != NULL); - fail_unless (g_list_length (pads) == 1); - - /* fakeHTTPsrc element is placed inside a bin. Get the bin */ - srcBin = gst_object_get_parent (msg->src); - - /* the bin should have only 1 output pad */ - pads = GST_ELEMENT_PADS (srcBin); - fail_unless (pads != NULL); - fail_unless (g_list_length (pads) == 1); - - internalPad = gst_pad_get_peer (GST_PAD (pads->data)); - testOutputStreamData = getTestOutputDataByPad (testData, internalPad); - gst_object_unref (internalPad); - - if (strcmp (testOutputStreamData->name, "audio_00") == 0) { - g_mutex_lock (&testData->scratchData->seekTaskStateLock); - if (testData->scratchData->seekTaskState == - SEEK_TASK_STATE_WAITING_FOR_FAKESRC_STATE_CHANGE) { - GST_DEBUG ("changing seekTaskState"); - testData->scratchData->seekTaskState = SEEK_TASK_STATE_EXITING; - g_cond_signal (&testData->scratchData->seekTaskStateCond); - } - g_mutex_unlock (&testData->scratchData->seekTaskStateLock); - } - gst_object_unref (srcBin); - } -} - -static void -testSeekPreTestCallback (GstDashDemuxTestData * testData) -{ - GstBus *bus; - - /* media segment starts at 4687 - * Issue a seek request after media segment has started to be downloaded - * on audio_00 stream and the first chunk of GST_FAKE_SOUP_HTTP_SRC_MAX_BUF_SIZE - * has already arrived in AppSink - */ - testData->scratchData->thresholdForSeek = 4687 + - GST_FAKE_SOUP_HTTP_SRC_MAX_BUF_SIZE; - - g_mutex_init (&testData->scratchData->seekTaskStateLock); - g_cond_init (&testData->scratchData->seekTaskStateCond); - - /* register a callback to listen for state change events */ - bus = gst_pipeline_get_bus (GST_PIPELINE (testData->scratchData->pipeline)); - gst_bus_add_signal_watch (bus); - g_signal_connect (bus, "message::state-changed", - G_CALLBACK (testSeekOnStateChanged), testData); -} - -/* function to do test seek cleanup */ -static void -testSeekPostTestCallback (GstDashDemuxTestData * testData) -{ - GstDashDemuxTestScratchData *scratchData = testData->scratchData; - - fail_if (scratchData->seekTask == NULL, - "seek test did not create task to perform the seek"); - gst_task_stop (scratchData->seekTask); - gst_task_join (scratchData->seekTask); - GST_DEBUG ("task stopped"); - gst_object_unref (scratchData->seekTask); - g_rec_mutex_clear (&scratchData->seekTaskLock); - - g_cond_clear (&testData->scratchData->seekTaskStateCond); - g_mutex_clear (&testData->scratchData->seekTaskStateLock); -} - /* * Test seeking * @@ -1205,41 +462,58 @@ GST_START_TEST (testSeek) " " " " " "; - - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, {"http://unit.test/audio.webm", NULL, 10000}, {NULL, NULL, 0}, }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { {"audio_00", 10000, NULL}, }; + GstAdaptiveDemuxTestCase *testData; - Callbacks cb = { 0 }; - cb.appSinkGotDataCallback = checkDataReceived; - cb.appSinkGotEosCallback = testSeekCheckSizeOfDataReceived; - cb.preTestCallback = testSeekPreTestCallback; - cb.postTestCallback = testSeekPostTestCallback; - cb.dashdemuxSendsDataCallback = testSeekDashdemuxSendsData; + testData = gst_adaptive_demux_test_case_new (); - runTest (inputTestData, - outputTestData, sizeof (outputTestData) / sizeof (outputTestData[0]), - &cb); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = gst_dashdemux_http_src_create; + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + + /* media segment starts at 4687 + * Issue a seek request after media segment has started to be downloaded + * on the first pad listed in GstAdaptiveDemuxTestOutputStreamData and the + * first chunk of at least one byte has already arrived in AppSink + */ + testData->threshold_for_seek = 4687 + 1; + + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); + gst_adaptive_demux_test_seek (DEMUX_ELEMENT_NAME, + "http://unit.test/test.mpd", testData); + gst_object_unref (testData); } GST_END_TEST; static void -testDownloadErrorPreTestCallback (GstDashDemuxTestData * testData) +testDownloadErrorMessageCallback (GstAdaptiveDemuxTestEngine * engine, + GstMessage * msg, gpointer user_data) { - /* expect error on pipeline */ - testData->expectError = TRUE; + GError *err = NULL; + gchar *dbg_info = NULL; + + fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR); + gst_message_parse_error (msg, &err, &dbg_info); + GST_DEBUG ("Error from element %s : %s\n", + GST_OBJECT_NAME (msg->src), err->message); + fail_unless_equals_string (GST_OBJECT_NAME (msg->src), DEMUX_ELEMENT_NAME); + /*GST_DEBUG ("dbg_info=%s\n", dbg_info); */ + g_error_free (err); + g_free (dbg_info); + g_main_loop_quit (engine->loop); } /* - * Test error download - * + * Test error case of failing to download a segment */ GST_START_TEST (testDownloadError) { @@ -1271,151 +545,40 @@ GST_START_TEST (testDownloadError) " " " "; - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, {NULL, NULL, 0}, }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { - {"audio_00", 5000, NULL}, + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { + {"audio_00", 0, NULL}, }; + GstAdaptiveDemuxTestCallbacks test_callbacks = { 0 }; + GstAdaptiveDemuxTestCase *testData; - Callbacks cb = { 0 }; - cb.appSinkGotDataCallback = checkDataReceived; - cb.appSinkGotEosCallback = checkSizeOfDataReceived; - cb.preTestCallback = testDownloadErrorPreTestCallback; + testData = gst_adaptive_demux_test_case_new (); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = gst_dashdemux_http_src_create; + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + test_callbacks.appsink_received_data = + gst_adaptive_demux_test_check_received_data; + test_callbacks.bus_error_message = testDownloadErrorMessageCallback; + test_callbacks.appsink_eos = + gst_adaptive_demux_test_check_size_of_received_data; - runTest (inputTestData, outputTestData, - sizeof (outputTestData) / sizeof (outputTestData[0]), &cb); + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); + gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME, "http://unit.test/test.mpd", + &test_callbacks, testData); + gst_object_unref (testData); } GST_END_TEST; -/* testFragmentDownloadError is disabled until we redesign the test framework - * to allow better control on when fakeHTTPsrc element will create data. - * Currently that is being done asynchronously and there is no easy way - * to synchronise data generation with the data processing (the test) so that - * we can guarantee that error generation is done during data generation and - * not afterwards. - */ -#if 0 -/* generate error message on adaptive demux pipeline */ -static gboolean -testFragmentDownloadErrorCheckDataReceived (GstDashDemuxTestData * testData, - GstDashDemuxTestOutputStreamData * testOutputStreamData, GstBuffer * buffer) -{ - checkDataReceived (testData, testOutputStreamData, buffer); - - if (testOutputStreamData->scratchData->segmentReceivedSize > 2000) { - GstPad *srcBinPad; - GstObject *srcBin; - GstElement *fakeHttpSrcElement; - - srcBinPad = - gst_pad_get_peer (testOutputStreamData->scratchData->internalPad); - srcBin = gst_pad_get_parent (srcBinPad); - - fakeHttpSrcElement = gst_bin_get_by_interface (GST_BIN (srcBin), - GST_TYPE_URI_HANDLER); - - /* tell fake soup http src to post an error on the adaptive demux bus */ - gst_fake_soup_http_src_simulate_download_error ((GstFakeSoupHTTPSrc *) - fakeHttpSrcElement, 404); - - gst_object_unref (srcBinPad); - gst_object_unref (srcBin); - gst_object_unref (fakeHttpSrcElement); - - testData->expectError = TRUE; - } - - return TRUE; -} - -/* function to check total size of data received by AppSink - * will be called when AppSink receives eos. - */ -static gboolean -testFragmentDownloadErrorCheckSizeOfDataReceived (GstDashDemuxTestData * - testData, GstDashDemuxTestOutputStreamData * testOutputStreamData) -{ - /* expect to receive more than 0 */ - fail_unless (testOutputStreamData->scratchData->totalReceivedSize > 0, - "size validation failed for %s, expected > 0, received %d", - testOutputStreamData->name, - testOutputStreamData->scratchData->totalReceivedSize); - - /* expect to receive less than file size */ - fail_unless (testOutputStreamData->scratchData->totalReceivedSize < - testOutputStreamData->expectedSize, - "size validation failed for %s, expected < %d received %d", - testOutputStreamData->name, testOutputStreamData->expectedSize, - testOutputStreamData->scratchData->totalReceivedSize); - - return TRUE; -} - -/* - * Test fragment download error - * Let the adaptive demux download a few bytes, then instruct the fake soup http - * src element to generate an error. - */ -GST_START_TEST (testFragmentDownloadError) -{ - const gchar *mpd = - "" - "" - " " - " " - " " - " " - " audio.webm" - " " - " " - " " - " "; - - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, - {"http://unit.test/audio.webm", NULL, 5000}, - {NULL, NULL, 0}, - }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { - {"audio_00", 5000, NULL}, - }; - - Callbacks cb = { 0 }; - cb.appSinkGotDataCallback = testFragmentDownloadErrorCheckDataReceived; - cb.appSinkGotEosCallback = testFragmentDownloadErrorCheckSizeOfDataReceived; - - runTest (inputTestData, outputTestData, - sizeof (outputTestData) / sizeof (outputTestData[0]), &cb); -} - -GST_END_TEST; - -#endif - /* generate queries to adaptive demux */ static gboolean -testQueryCheckDataReceived (GstDashDemuxTestData * testData, - GstDashDemuxTestOutputStreamData * testOutputStreamData, GstBuffer * buffer) +testQueryCheckDataReceived (GstAdaptiveDemuxTestEngine * engine, + GstAdaptiveDemuxTestOutputStream * stream, + GstBuffer * buffer, gpointer user_data) { GList *pads; GstPad *pad; @@ -1429,7 +592,7 @@ testQueryCheckDataReceived (GstDashDemuxTestData * testData, gchar *redirect_uri; gboolean redirect_permanent; - pads = GST_ELEMENT_PADS (testOutputStreamData->scratchData->appsink); + pads = GST_ELEMENT_PADS (stream->appsink); /* AppSink should have only 1 pad */ fail_unless (pads != NULL); @@ -1467,7 +630,8 @@ testQueryCheckDataReceived (GstDashDemuxTestData * testData, g_free (redirect_uri); gst_query_unref (query); - return checkDataReceived (testData, testOutputStreamData, buffer); + return gst_adaptive_demux_test_check_received_data (engine, + stream, buffer, user_data); } /* @@ -1503,24 +667,142 @@ GST_START_TEST (testQuery) " " " " " "; - - const GstFakeHttpSrcInputData inputTestData[] = { - {"http://unit.test/test.mpd", mpd, 0}, + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, {"http://unit.test/audio.webm", NULL, 5000}, {NULL, NULL, 0}, }; - - GstDashDemuxTestOutputStreamData outputTestData[] = { + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { {"audio_00", 5000, NULL}, }; + GstAdaptiveDemuxTestCallbacks test_callbacks = { 0 }; + GstAdaptiveDemuxTestCase *testData; - Callbacks cb = { 0 }; - cb.appSinkGotDataCallback = testQueryCheckDataReceived; - cb.appSinkGotEosCallback = checkSizeOfDataReceived; + testData = gst_adaptive_demux_test_case_new (); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = gst_dashdemux_http_src_create; + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + test_callbacks.appsink_received_data = testQueryCheckDataReceived; + test_callbacks.appsink_eos = + gst_adaptive_demux_test_check_size_of_received_data; - runTest (inputTestData, - outputTestData, sizeof (outputTestData) / sizeof (outputTestData[0]), - &cb); + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); + gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME, + "http://unit.test/test.mpd", &test_callbacks, testData); + gst_object_unref (testData); +} + +GST_END_TEST; + +static GstFlowReturn +test_fragment_download_error_src_create (GstTestHTTPSrc * src, + guint64 offset, + guint length, GstBuffer ** retbuf, gpointer context, gpointer user_data) +{ + const GstDashDemuxTestInputData *input = + (const GstDashDemuxTestInputData *) context; + fail_unless (input != NULL); + if (!g_str_has_suffix (input->uri, ".mpd") && offset > 2000) { + GST_DEBUG ("network_error %s %" G_GUINT64_FORMAT " @ %d", + input->uri, offset, 2000); + GST_ELEMENT_ERROR (src, RESOURCE, READ, + (("A network error occurred, or the server closed the connection unexpectedly.")), ("A network error occurred, or the server closed the connection unexpectedly.")); + return GST_FLOW_ERROR; + } + return gst_dashdemux_http_src_create (src, offset, length, retbuf, context, + user_data); +} + +/* function to check total size of data received by AppSink + * will be called when AppSink receives eos. + */ +static void +testFragmentDownloadErrorCheckSizeOfDataReceived (GstAdaptiveDemuxTestEngine * + engine, GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data) +{ + GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data); + GstAdaptiveDemuxTestExpectedOutput *testOutputStreamData; + + testOutputStreamData = + gst_adaptive_demux_test_find_test_data_by_stream (testData, stream, NULL); + fail_unless (testOutputStreamData != NULL); + + /* expect to receive more than 0 */ + fail_unless (stream->total_received_size > 0, + "size validation failed for %s, expected > 0, received %d", + testOutputStreamData->name, stream->total_received_size); + + /* expect to receive less than file size */ + fail_unless (stream->total_received_size < + testOutputStreamData->expected_size, + "size validation failed for %s, expected < %d received %d", + testOutputStreamData->name, testOutputStreamData->expected_size, + stream->total_received_size); +} + +/* + * Test fragment download error + * Let the adaptive demux download a few bytes, then instruct the + * GstTestHTTPSrc element to generate an error. + */ +GST_START_TEST (testFragmentDownloadError) +{ + const gchar *mpd = + "" + "" + " " + " " + " " + " " + " audio.webm" + " " + " " + " " + " "; + + GstDashDemuxTestInputData inputTestData[] = { + {"http://unit.test/test.mpd", (guint8 *) mpd, 0}, + {"http://unit.test/audio.webm", NULL, 5000}, + {NULL, NULL, 0}, + }; + GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; + GstAdaptiveDemuxTestExpectedOutput outputTestData[] = { + {"audio_00", 5000, NULL}, + }; + GstAdaptiveDemuxTestCallbacks test_callbacks = { 0 }; + GstAdaptiveDemuxTestCase *testData; + + testData = gst_adaptive_demux_test_case_new (); + http_src_callbacks.src_start = gst_dashdemux_http_src_start; + http_src_callbacks.src_create = test_fragment_download_error_src_create; + COPY_OUTPUT_TEST_DATA (outputTestData, testData); + test_callbacks.appsink_received_data = + gst_adaptive_demux_test_check_received_data; + test_callbacks.appsink_eos = testFragmentDownloadErrorCheckSizeOfDataReceived; + /* test_callbacks.demux_sent_eos = gst_adaptive_demux_test_check_size_of_received_data; */ + + test_callbacks.bus_error_message = testDownloadErrorMessageCallback; + + gst_test_http_src_install_callbacks (&http_src_callbacks, inputTestData); + gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME, + "http://unit.test/test.mpd", &test_callbacks, testData); + gst_object_unref (testData); } GST_END_TEST; @@ -1536,10 +818,11 @@ dash_demux_suite (void) tcase_add_test (tc_basicTest, testParameters); tcase_add_test (tc_basicTest, testSeek); tcase_add_test (tc_basicTest, testDownloadError); - //tcase_add_test (tc_basicTest, testFragmentDownloadError); + tcase_add_test (tc_basicTest, testFragmentDownloadError); tcase_add_test (tc_basicTest, testQuery); - tcase_add_unchecked_fixture (tc_basicTest, test_setup, test_teardown); + tcase_add_unchecked_fixture (tc_basicTest, gst_adaptive_demux_test_setup, + gst_adaptive_demux_test_teardown); suite_add_tcase (s, tc_basicTest); diff --git a/tests/check/elements/fake_http_src.c b/tests/check/elements/fake_http_src.c deleted file mode 100644 index a5575db905..0000000000 --- a/tests/check/elements/fake_http_src.c +++ /dev/null @@ -1,511 +0,0 @@ -/* GStreamer unit test for MPEG-DASH - * - * Copyright (c) <2015> YouView TV Ltd - * - * 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. - */ - -#include -#include - -#include "fake_http_src.h" - -#ifndef GST_PACKAGE_NAME -#define GST_PACKAGE_NAME "gst-plugins-bad" -#endif - -#ifndef GST_PACKAGE_ORIGIN -#define GST_PACKAGE_ORIGIN "https://developer.gnome.org/gstreamer/" -#endif - -#define GST_FAKE_SOUP_HTTP_SRC(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_FAKE_SOUP_HTTP_SRC, GstFakeSoupHTTPSrc)) -#define GST_FAKE_SOUP_HTTP_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_FAKE_SOUP_HTTP_SRC, GstFakeSoupHTTPSrcClass)) -#define GST_IS_FAKE_SOUP_HTTP_SRC(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_FAKE_SOUP_HTTP_SRC)) -#define GST_IS_FAKE_SOUP_HTTP_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_FAKE_SOUP_HTTP_SRC)) -#define GST_FAKE_SOUP_HTTP_SRC_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_FAKE_SOUP_HTTP_SRC, GstFakeSoupHTTPSrcClass)) - -#define GST_FAKE_SOUP_HTTP_SRC_GET_LOCK(d) (&(((GstFakeSoupHTTPSrc*)(d))->lock)) -#define GST_FAKE_SOUP_HTTP_SRC_LOCK(d) g_mutex_lock (GST_FAKE_SOUP_HTTP_SRC_GET_LOCK (d)) -#define GST_FAKE_SOUP_HTTP_SRC_UNLOCK(d) g_mutex_unlock (GST_FAKE_SOUP_HTTP_SRC_GET_LOCK (d)) - -typedef struct _GstFakeSoupHTTPSrc -{ - GstBaseSrc parent; - - /* uri for which to retrieve data */ - gchar *uri; - /* data to retrieve. - * If NULL, we will fake a buffer of size bytes, containing numbers in sequence - * 0, 4, 8, ... - * Each number is written on sizeof(int) bytes in little endian format - */ - const gchar *payload; - /* size of data to generate */ - guint size; - /* position from where to retrieve data */ - guint64 position; - /* index immediately after the last byte from the segment to be retrieved */ - guint64 segment_end; - - /* download error code to simulate during create function */ - guint downloadErrorCode; - - /* mutex to protect multithread access to this structure */ - GMutex lock; -} GstFakeSoupHTTPSrc; - -typedef struct _GstFakeSoupHTTPSrcClass -{ - GstBaseSrcClass parent_class; -} GstFakeSoupHTTPSrcClass; - -typedef struct _PluginInitContext -{ - const gchar *name; - guint rank; - GType type; -} PluginInitContext; - - -static const GstFakeHttpSrcInputData *gst_fake_soup_http_src_inputData = NULL; - -static GstStaticPadTemplate gst_dashdemux_test_source_template = -GST_STATIC_PAD_TEMPLATE ("src", - GST_PAD_SRC, - GST_PAD_ALWAYS, - GST_STATIC_CAPS_ANY); - -static void gst_fake_soup_http_src_uri_handler_init (gpointer g_iface, - gpointer iface_data); -static void gst_fake_soup_http_src_finalize (GObject * object); -static gboolean gst_fake_soup_http_src_is_seekable (GstBaseSrc * basesrc); -static gboolean gst_fake_soup_http_src_do_seek (GstBaseSrc * basesrc, - GstSegment * segment); -static gboolean gst_fake_soup_http_src_start (GstBaseSrc * basesrc); -static gboolean gst_fake_soup_http_src_stop (GstBaseSrc * basesrc); -static gboolean gst_fake_soup_http_src_get_size (GstBaseSrc * basesrc, - guint64 * size); -static GstFlowReturn gst_fake_soup_http_src_create (GstBaseSrc * basesrc, - guint64 offset, guint length, GstBuffer ** ret); - -#define _do_init \ - G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER, gst_fake_soup_http_src_uri_handler_init); - -#define gst_fake_soup_http_src_parent_class parent_class -G_DEFINE_TYPE_WITH_CODE (GstFakeSoupHTTPSrc, gst_fake_soup_http_src, - GST_TYPE_BASE_SRC, _do_init); - -static void -gst_fake_soup_http_src_class_init (GstFakeSoupHTTPSrcClass * klass) -{ - GObjectClass *gobject_class; - GstElementClass *gstelement_class; - GstBaseSrcClass *gstbasesrc_class; - - gobject_class = G_OBJECT_CLASS (klass); - gstelement_class = GST_ELEMENT_CLASS (klass); - gstbasesrc_class = GST_BASE_SRC_CLASS (klass); - - gobject_class->finalize = gst_fake_soup_http_src_finalize; - - gst_element_class_set_metadata (gstelement_class, - "Fake HTTP source element for unit tests", - "Source/Network", - "Use in unit tests", "Alex Ashley "); - gst_element_class_add_pad_template (gstelement_class, - gst_static_pad_template_get (&gst_dashdemux_test_source_template)); - - gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_fake_soup_http_src_start); - gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_fake_soup_http_src_stop); - gstbasesrc_class->is_seekable = - GST_DEBUG_FUNCPTR (gst_fake_soup_http_src_is_seekable); - gstbasesrc_class->do_seek = - GST_DEBUG_FUNCPTR (gst_fake_soup_http_src_do_seek); - gstbasesrc_class->get_size = - GST_DEBUG_FUNCPTR (gst_fake_soup_http_src_get_size); - gstbasesrc_class->create = GST_DEBUG_FUNCPTR (gst_fake_soup_http_src_create); - -} - -static void -gst_fake_soup_http_src_init (GstFakeSoupHTTPSrc * src) -{ - src->uri = NULL; - src->payload = NULL; - src->position = 0; - src->size = 0; - src->segment_end = 0; - src->downloadErrorCode = 0; - gst_base_src_set_blocksize (GST_BASE_SRC (src), - GST_FAKE_SOUP_HTTP_SRC_MAX_BUF_SIZE); - g_mutex_init (&src->lock); -} - -static void -gst_fake_soup_http_src_finalize (GObject * object) -{ - GstFakeSoupHTTPSrc *src; - - src = GST_FAKE_SOUP_HTTP_SRC (object); - - g_mutex_clear (&src->lock); - g_free (src->uri); - - G_OBJECT_CLASS (parent_class)->finalize (object); -} - -static gboolean -gst_fake_soup_http_src_start (GstBaseSrc * basesrc) -{ - GstFakeSoupHTTPSrc *src; - const GstFakeHttpSrcInputData *input = gst_fake_soup_http_src_inputData; - - src = GST_FAKE_SOUP_HTTP_SRC (basesrc); - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - if (!src->uri) { - GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, (("No URL set.")), - ("Missing location property")); - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; - } - - for (guint i = 0; input[i].uri; ++i) { - if (strcmp (input[i].uri, src->uri) == 0) { - src->payload = input[i].payload; - src->position = 0; - if (src->payload) - src->size = strlen (src->payload); - else - src->size = input[i].size; - src->segment_end = src->size; - src->downloadErrorCode = 0; - gst_base_src_set_dynamic_size (basesrc, FALSE); - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return TRUE; - } - } - - GST_WARNING - ("gst_fake_soup_http_src_start cannot find url '%s' in input data", - src->uri); - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; -} - -static gboolean -gst_fake_soup_http_src_stop (GstBaseSrc * basesrc) -{ - GstFakeSoupHTTPSrc *src; - - src = GST_FAKE_SOUP_HTTP_SRC (basesrc); - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - src->payload = NULL; - src->position = 0; - src->size = 0; - - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return TRUE; -} - -static gboolean -gst_fake_soup_http_src_is_seekable (GstBaseSrc * basesrc) -{ - GstFakeSoupHTTPSrc *src; - gboolean ret; - - src = GST_FAKE_SOUP_HTTP_SRC (basesrc); - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - /* if size is set, we can seek */ - ret = src->size > 0; - - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - - return ret; -} - -static gboolean -gst_fake_soup_http_src_do_seek (GstBaseSrc * basesrc, GstSegment * segment) -{ - GstFakeSoupHTTPSrc *src; - - GST_DEBUG ("gst_fake_soup_http_src_do_seek start = %" G_GUINT64_FORMAT, - segment->start); - src = GST_FAKE_SOUP_HTTP_SRC (basesrc); - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - /* - According to RFC7233, the range is inclusive: - The first-byte-pos value in a byte-range-spec gives the byte-offset - of the first byte in a range. The last-byte-pos value gives the - byte-offset of the last byte in the range; that is, the byte - positions specified are inclusive. Byte offsets start at zero. - */ - - if (!src->uri) { - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; - } - - if (segment->start >= src->size) { - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; - } - - if (segment->stop != -1 && segment->stop > src->size) { - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; - } - - src->position = segment->start; - - if (segment->stop != -1) { - src->segment_end = segment->stop; - } - - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return TRUE; -} - -static gboolean -gst_fake_soup_http_src_get_size (GstBaseSrc * basesrc, guint64 * size) -{ - GstFakeSoupHTTPSrc *src; - const GstFakeHttpSrcInputData *input = gst_fake_soup_http_src_inputData; - - src = GST_FAKE_SOUP_HTTP_SRC (basesrc); - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - if (!src->uri) { - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; - } - - /* if it was started (payload or size configured), size is set */ - if (src->payload || src->size > 0) { - *size = src->size; - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return TRUE; - } - - /* it wasn't started, compute the size */ - for (guint i = 0; input[i].uri; ++i) { - if (strcmp (input[i].uri, src->uri) == 0) { - if (input[i].payload) - *size = strlen (input[i].payload); - else - *size = input[i].size; - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return TRUE; - } - } - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return FALSE; -} - -static GstFlowReturn -gst_fake_soup_http_src_create (GstBaseSrc * basesrc, guint64 offset, - guint length, GstBuffer ** ret) -{ - GstFakeSoupHTTPSrc *src; - guint bytes_read; - GstBuffer *buf; - - src = GST_FAKE_SOUP_HTTP_SRC (basesrc); - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - GST_DEBUG ("gst_fake_soup_http_src_create feeding from %" G_GUINT64_FORMAT, - src->position); - if (src->uri == NULL) { - GST_ELEMENT_ERROR (src, RESOURCE, READ, (NULL), GST_ERROR_SYSTEM); - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return GST_FLOW_ERROR; - } - if (src->downloadErrorCode) { - GST_ELEMENT_ERROR (src, RESOURCE, NOT_FOUND, ("%s", - "Generated requested error"), ("%s (%d), URL: %s, Redirect to: %s", - "Generated requested error", src->downloadErrorCode, src->uri, - GST_STR_NULL (NULL))); - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return GST_FLOW_ERROR; - } - - bytes_read = MIN ((src->segment_end - src->position), - GST_FAKE_SOUP_HTTP_SRC_MAX_BUF_SIZE); - if (bytes_read == 0) { - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return GST_FLOW_EOS; - } - - buf = gst_buffer_new_allocate (NULL, bytes_read, NULL); - fail_if (buf == NULL, "Not enough memory to allocate buffer"); - - if (src->payload) { - gst_buffer_fill (buf, 0, src->payload + src->position, bytes_read); - } else { - GstMapInfo info; - guint pattern; - - pattern = src->position - src->position % sizeof (pattern); - - gst_buffer_map (buf, &info, GST_MAP_WRITE); - for (guint64 i = 0; i < bytes_read; ++i) { - gchar pattern_byte_to_write = (src->position + i) % sizeof (pattern); - if (pattern_byte_to_write == 0) { - pattern = src->position + i; - } - info.data[i] = (pattern >> (pattern_byte_to_write * 8)) & 0xFF; - } - gst_buffer_unmap (buf, &info); - } - - GST_BUFFER_OFFSET (buf) = src->position; - GST_BUFFER_OFFSET_END (buf) = src->position + bytes_read; - *ret = buf; - - src->position += bytes_read; - - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return GST_FLOW_OK; -} - -/* must be called with the lock taken */ -static gboolean -gst_fake_soup_http_src_set_location (GstFakeSoupHTTPSrc * src, - const gchar * uri, GError ** error) -{ - g_free (src->uri); - src->uri = g_strdup (uri); - return TRUE; -} - -static GstURIType -gst_fake_soup_http_src_uri_get_type (GType type) -{ - return GST_URI_SRC; -} - -static const gchar *const * -gst_fake_soup_http_src_uri_get_protocols (GType type) -{ - static const gchar *protocols[] = { "http", NULL }; - - return protocols; -} - -static gchar * -gst_fake_soup_http_src_uri_get_uri (GstURIHandler * handler) -{ - GstFakeSoupHTTPSrc *src = GST_FAKE_SOUP_HTTP_SRC (handler); - gchar *uri; - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - uri = g_strdup (src->uri); - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - return uri; -} - -static gboolean -gst_fake_soup_http_src_uri_set_uri (GstURIHandler * handler, const gchar * uri, - GError ** err) -{ - GstFakeSoupHTTPSrc *src = GST_FAKE_SOUP_HTTP_SRC (handler); - gboolean ret; - - GST_FAKE_SOUP_HTTP_SRC_LOCK (src); - - ret = gst_fake_soup_http_src_set_location (src, uri, err); - - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (src); - - return ret; -} - -static void -gst_fake_soup_http_src_uri_handler_init (gpointer g_iface, gpointer iface_data) -{ - GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface; - - iface->get_type = gst_fake_soup_http_src_uri_get_type; - iface->get_protocols = gst_fake_soup_http_src_uri_get_protocols; - iface->get_uri = gst_fake_soup_http_src_uri_get_uri; - iface->set_uri = gst_fake_soup_http_src_uri_set_uri; -} - -static gboolean -gst_fake_soup_http_src_plugin_init_func (GstPlugin * plugin, gpointer user_data) -{ - PluginInitContext *context = (PluginInitContext *) user_data; - gboolean ret; - - ret = - gst_element_register (plugin, context->name, context->rank, - context->type); - return ret; -} - -gboolean -gst_fake_soup_http_src_register_plugin (GstRegistry * registry, - const gchar * name) -{ - gboolean ret; - PluginInitContext context; - - context.name = name; - context.rank = GST_RANK_PRIMARY + 1; - context.type = GST_TYPE_FAKE_SOUP_HTTP_SRC; - ret = gst_plugin_register_static_full (GST_VERSION_MAJOR, /* version */ - GST_VERSION_MINOR, /* version */ - name, /* name */ - "Replaces a souphttpsrc plugin and returns predefined data.", /* description */ - gst_fake_soup_http_src_plugin_init_func, /* init function */ - "0.0.0", /* version string */ - GST_LICENSE_UNKNOWN, /* license */ - __FILE__, /* source */ - GST_PACKAGE_NAME, /* package */ - GST_PACKAGE_ORIGIN, /* origin */ - &context /* user_data */ - ); - return ret; -} - -/** - * gst_fake_soup_http_src_set_input_data: - * @input: array of #GstFakeHttpSrcInputData that is used when - * responding to a request. The last entry in the array must - * have the uri field set to NULL - */ -void -gst_fake_soup_http_src_set_input_data (const GstFakeHttpSrcInputData * input) -{ - gst_fake_soup_http_src_inputData = input; -} - -void -gst_fake_soup_http_src_simulate_download_error (GstFakeSoupHTTPSrc * - fakeSoupHTTPSrc, guint downloadErrorCode) -{ - GST_FAKE_SOUP_HTTP_SRC_LOCK (fakeSoupHTTPSrc); - fakeSoupHTTPSrc->downloadErrorCode = downloadErrorCode; - GST_FAKE_SOUP_HTTP_SRC_UNLOCK (fakeSoupHTTPSrc); -} diff --git a/tests/check/elements/fake_http_src.h b/tests/check/elements/fake_http_src.h deleted file mode 100644 index abcaa169c6..0000000000 --- a/tests/check/elements/fake_http_src.h +++ /dev/null @@ -1,61 +0,0 @@ -/* Fake HTTP source element - * - * Copyright (c) <2015> YouView TV Ltd - * - * 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. - */ - -#include - -G_BEGIN_DECLS - -#define GST_TYPE_FAKE_SOUP_HTTP_SRC (gst_fake_soup_http_src_get_type ()) - -/* structure used by tests to configure the GstFakeSoupHTTPSrc plugin. - * It specifies what data to be fed for the given uri. - * For the requested uri, it will return the data from payload. - * If the payload is NULL, it will fake a buffer of size bytes and return data from it. - * The buffer will contain a pattern, numbers 0, 4, 8, ... etc written on - * sizeof(int) bytes, in little endian format (eg if sizeof(int)=4, the first 12 - * bytes are 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00) - * Size is used only if payload is NULL. - */ -typedef struct _GstFakeHttpSrcInputData -{ - /* the uri for which data is being requested */ - const gchar *uri; - /* the payload to be returned */ - const gchar *payload; - /* the size of data to fake if payload is NULL */ - guint64 size; -} GstFakeHttpSrcInputData; - -typedef struct _GstFakeSoupHTTPSrc GstFakeSoupHTTPSrc; - -/* GstFakeSoupHTTPSrc will send buffers up to this size */ -#define GST_FAKE_SOUP_HTTP_SRC_MAX_BUF_SIZE (1024) - -GType gst_fake_soup_http_src_get_type (void); - -gboolean gst_fake_soup_http_src_register_plugin (GstRegistry * registry, const gchar * name); - -void gst_fake_soup_http_src_set_input_data (const GstFakeHttpSrcInputData *input); - -/* TODO: use SoupKnownStatusCode. But it requires makefile support to include - * - */ -void gst_fake_soup_http_src_simulate_download_error ( - GstFakeSoupHTTPSrc *fakeSoupHTTPSrc, guint downloadErrorCode); diff --git a/tests/check/elements/test_http_src.c b/tests/check/elements/test_http_src.c new file mode 100644 index 0000000000..88b0c4f21e --- /dev/null +++ b/tests/check/elements/test_http_src.c @@ -0,0 +1,716 @@ +/* HTTP source element for use in tests + * + * Copyright (c) <2015> YouView TV Ltd + * + * 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. + */ + +#include +#include + +#include "test_http_src.h" + +#ifndef GST_PACKAGE_NAME +#define GST_PACKAGE_NAME "gst-plugins-bad" +#endif + +#ifndef GST_PACKAGE_ORIGIN +#define GST_PACKAGE_ORIGIN "https://developer.gnome.org/gstreamer/" +#endif + +#define GST_TEST_HTTP_SRC(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_TEST_HTTP_SRC, GstTestHTTPSrc)) +#define GST_TEST_HTTP_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_TEST_HTTP_SRC, GstTestHTTPSrcClass)) +#define GST_IS_TEST_HTTP_SRC(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_TEST_HTTP_SRC)) +#define GST_IS_TEST_HTTP_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_TEST_HTTP_SRC)) +#define GST_TEST_HTTP_SRC_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_TEST_HTTP_SRC, GstTestHTTPSrcClass)) + +#define DEFAULT_USER_AGENT "GStreamer testhttpsrc " +#define DEFAULT_COMPRESS FALSE +#define DEFAULT_HTTP_METHOD NULL +#define DEFAULT_KEEP_ALIVE FALSE + +enum +{ + PROP_0, + PROP_USER_AGENT, + PROP_EXTRA_HEADERS, + PROP_COMPRESS, + PROP_KEEP_ALIVE, + PROP_METHOD, + PROP_LAST +}; + +typedef enum +{ + METHOD_INVALID, + METHOD_GET, + METHOD_POST, + METHOD_HEAD, + METHOD_OPTIONS +} HttpMethod; + +typedef struct _GstTestHTTPSrcMethodName +{ + const gchar *name; + HttpMethod method; +} GstTestHTTPSrcMethodName; + +static const GstTestHTTPSrcMethodName gst_test_http_src_methods[] = { + {"GET", METHOD_GET}, + {"POST", METHOD_POST}, + {"HEAD", METHOD_HEAD}, + {"OPTIONS", METHOD_OPTIONS}, + {NULL, METHOD_INVALID} +}; + +typedef struct _GstTestHTTPSrc +{ + GstBaseSrc parent; + + GMutex mutex; + + GstTestHTTPSrcInput input; + + gchar *uri; /* the uri for which data is being requested */ + gboolean compress; + gboolean keep_alive; + gchar *http_method_name; + HttpMethod http_method; + GstStructure *extra_headers; + gchar *user_agent; + + guint64 position; + /* index immediately after the last byte from the segment to be retrieved */ + guint64 segment_end; + + GstEvent *http_headers_event; + gboolean duration_changed; +} GstTestHTTPSrc; + +typedef struct _GstTestHTTPSrcClass +{ + GstBaseSrcClass parent_class; +} GstTestHTTPSrcClass; + +typedef struct _PluginInitContext +{ + const gchar *name; + guint rank; + GType type; +} PluginInitContext; + +static const GstTestHTTPSrcCallbacks *gst_test_http_src_callbacks = NULL; +static gpointer gst_test_http_src_callback_user_data = NULL; +static guint gst_test_http_src_blocksize = 0; + +static GstStaticPadTemplate gst_dashdemux_test_source_template = +GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS_ANY); + +static void gst_test_http_src_uri_handler_init (gpointer g_iface, + gpointer iface_data); +static void gst_test_http_src_finalize (GObject * object); +static gboolean gst_test_http_src_is_seekable (GstBaseSrc * basesrc); +static gboolean gst_test_http_src_do_seek (GstBaseSrc * basesrc, + GstSegment * segment); +static gboolean gst_test_http_src_start (GstBaseSrc * basesrc); +static gboolean gst_test_http_src_stop (GstBaseSrc * basesrc); +static gboolean gst_test_http_src_get_size (GstBaseSrc * basesrc, + guint64 * size); +static GstFlowReturn gst_test_http_src_create (GstBaseSrc * basesrc, + guint64 offset, guint length, GstBuffer ** ret); +static void gst_test_http_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_test_http_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + + +#define _do_init \ + G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER, gst_test_http_src_uri_handler_init); + +#define gst_test_http_src_parent_class parent_class +G_DEFINE_TYPE_WITH_CODE (GstTestHTTPSrc, gst_test_http_src, + GST_TYPE_BASE_SRC, _do_init); + +static void +gst_test_http_src_class_init (GstTestHTTPSrcClass * klass) +{ + GObjectClass *gobject_class; + GstElementClass *gstelement_class; + GstBaseSrcClass *gstbasesrc_class; + + gobject_class = G_OBJECT_CLASS (klass); + gstelement_class = GST_ELEMENT_CLASS (klass); + gstbasesrc_class = GST_BASE_SRC_CLASS (klass); + + gobject_class->set_property = gst_test_http_src_set_property; + gobject_class->get_property = gst_test_http_src_get_property; + gobject_class->finalize = gst_test_http_src_finalize; + + g_object_class_install_property (gobject_class, PROP_COMPRESS, + g_param_spec_boolean ("compress", "Compress", + "Allow compressed content encodings", + DEFAULT_COMPRESS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, PROP_EXTRA_HEADERS, + g_param_spec_boxed ("extra-headers", "Extra Headers", + "Extra headers to append to the HTTP request", + GST_TYPE_STRUCTURE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, PROP_KEEP_ALIVE, + g_param_spec_boolean ("keep-alive", "keep-alive", + "Use HTTP persistent connections", DEFAULT_KEEP_ALIVE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, PROP_METHOD, + g_param_spec_string ("method", "HTTP method", + "The HTTP method to use (GET, HEAD, OPTIONS, etc)", + DEFAULT_HTTP_METHOD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_USER_AGENT, + g_param_spec_string ("user-agent", "User-Agent", + "Value of the User-Agent HTTP request header field", + DEFAULT_USER_AGENT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gst_element_class_set_metadata (gstelement_class, + "Test HTTP source element for unit tests", + "Source/Network", + "Use in unit tests", "Alex Ashley "); + gst_element_class_add_pad_template (gstelement_class, + gst_static_pad_template_get (&gst_dashdemux_test_source_template)); + + gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_test_http_src_start); + gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_test_http_src_stop); + gstbasesrc_class->is_seekable = + GST_DEBUG_FUNCPTR (gst_test_http_src_is_seekable); + gstbasesrc_class->do_seek = GST_DEBUG_FUNCPTR (gst_test_http_src_do_seek); + gstbasesrc_class->get_size = GST_DEBUG_FUNCPTR (gst_test_http_src_get_size); + gstbasesrc_class->create = GST_DEBUG_FUNCPTR (gst_test_http_src_create); + +} + +static void +gst_test_http_src_init (GstTestHTTPSrc * src) +{ + g_mutex_init (&src->mutex); + src->uri = NULL; + memset (&src->input, 0, sizeof (src->input)); + src->compress = FALSE; + src->keep_alive = FALSE; + src->http_method_name = NULL; + src->http_method = METHOD_GET; + src->user_agent = NULL; + src->position = 0; + src->segment_end = 0; + src->http_headers_event = NULL; + src->duration_changed = FALSE; + if (gst_test_http_src_blocksize) + gst_base_src_set_blocksize (GST_BASE_SRC (src), + gst_test_http_src_blocksize); +} + +static void +gst_test_http_src_reset_input (GstTestHTTPSrc * src) +{ + src->input.context = NULL; + src->input.size = 0; + src->input.status_code = 0; + if (src->input.request_headers) { + gst_structure_free (src->input.request_headers); + src->input.request_headers = NULL; + } + if (src->input.response_headers) { + gst_structure_free (src->input.response_headers); + src->input.response_headers = NULL; + } + if (src->http_headers_event) { + gst_event_unref (src->http_headers_event); + src->http_headers_event = NULL; + } + if (src->extra_headers) { + gst_structure_free (src->extra_headers); + src->extra_headers = NULL; + } + src->duration_changed = FALSE; +} + +static void +gst_test_http_src_finalize (GObject * object) +{ + GstTestHTTPSrc *src; + + src = GST_TEST_HTTP_SRC (object); + + g_free (src->uri); + gst_test_http_src_reset_input (src); + g_mutex_clear (&src->mutex); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static gboolean +gst_test_http_src_start (GstBaseSrc * basesrc) +{ + GstTestHTTPSrc *src; + GstStructure *http_headers; + + src = GST_TEST_HTTP_SRC (basesrc); + g_mutex_lock (&src->mutex); + gst_test_http_src_reset_input (src); + if (!src->uri) { + GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, (("No URL set.")), + ("Missing location property")); + g_mutex_unlock (&src->mutex); + return FALSE; + } + if (!gst_test_http_src_callbacks) { + GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, + (("Callbacks not registered.")), ("Callbacks not registered")); + g_mutex_unlock (&src->mutex); + return FALSE; + } + if (!gst_test_http_src_callbacks->src_start) { + GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, + (("src_start callback not defined.")), + ("src_start callback not registered")); + g_mutex_unlock (&src->mutex); + return FALSE; + } + if (!gst_test_http_src_callbacks->src_start (src, src->uri, &src->input, + gst_test_http_src_callback_user_data)) { + if (src->input.status_code == 0) { + src->input.status_code = 404; + } + } else { + if (src->input.status_code == 0) { + src->input.status_code = 200; + } + src->position = 0; + src->segment_end = src->input.size; + gst_base_src_set_dynamic_size (basesrc, FALSE); + basesrc->segment.duration = src->input.size; + src->duration_changed = TRUE; + } + http_headers = gst_structure_new_empty ("http-headers"); + gst_structure_set (http_headers, "uri", G_TYPE_STRING, src->uri, NULL); + if (!src->input.request_headers) { + src->input.request_headers = gst_structure_new_empty ("request-headers"); + } + if (!gst_structure_has_field_typed (src->input.request_headers, + "User-Agent", G_TYPE_STRING)) { + gst_structure_set (src->input.request_headers, + "User-Agent", G_TYPE_STRING, + src->user_agent ? src->user_agent : DEFAULT_USER_AGENT, NULL); + } + if (!gst_structure_has_field_typed (src->input.request_headers, + "Connection", G_TYPE_STRING)) { + gst_structure_set (src->input.request_headers, + "Connection", G_TYPE_STRING, + src->keep_alive ? "Keep-Alive" : "Close", NULL); + } + if (src->compress + && !gst_structure_has_field_typed (src->input.request_headers, + "Accept-Encoding", G_TYPE_STRING)) { + gst_structure_set (src->input.request_headers, "Accept-Encoding", + G_TYPE_STRING, "compress, gzip", NULL); + } + gst_structure_set (http_headers, "request-headers", GST_TYPE_STRUCTURE, + src->input.request_headers, NULL); + if (!src->input.response_headers) { + src->input.response_headers = gst_structure_new_empty ("response-headers"); + } + if (!gst_structure_has_field_typed (src->input.response_headers, + "Connection", G_TYPE_STRING)) { + gst_structure_set (src->input.response_headers, + "Connection", G_TYPE_STRING, + src->keep_alive ? "keep-alive" : "close", NULL); + } + if (!gst_structure_has_field_typed (src->input.response_headers, + "Date", G_TYPE_STRING)) { + GDateTime *now; + gchar *date_str; + + now = g_date_time_new_now_local (); + fail_unless (now != NULL); + date_str = g_date_time_format (now, "%a, %e %b %Y %T %Z"); + fail_unless (date_str != NULL); + gst_structure_set (src->input.response_headers, + "Date", G_TYPE_STRING, date_str, NULL); + g_free (date_str); + g_date_time_unref (now); + } + gst_structure_set (http_headers, "response-headers", GST_TYPE_STRUCTURE, + src->input.response_headers, NULL); + if (src->http_headers_event) { + gst_event_unref (src->http_headers_event); + } + src->http_headers_event = + gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY, http_headers); + g_mutex_unlock (&src->mutex); + return TRUE; +} + +static gboolean +gst_test_http_src_stop (GstBaseSrc * basesrc) +{ + GstTestHTTPSrc *src; + + src = GST_TEST_HTTP_SRC (basesrc); + g_mutex_lock (&src->mutex); + src->position = 0; + gst_test_http_src_reset_input (src); + g_mutex_unlock (&src->mutex); + return TRUE; +} + +static gboolean +gst_test_http_src_is_seekable (GstBaseSrc * basesrc) +{ + GstTestHTTPSrc *src; + gboolean ret; + + src = GST_TEST_HTTP_SRC (basesrc); + g_mutex_lock (&src->mutex); + /* if size is set, we can seek */ + ret = src->input.size > 0; + g_mutex_unlock (&src->mutex); + return ret; +} + +static gboolean +gst_test_http_src_do_seek (GstBaseSrc * basesrc, GstSegment * segment) +{ + GstTestHTTPSrc *src = GST_TEST_HTTP_SRC (basesrc); + + GST_DEBUG ("gst_test_http_src_do_seek start = %" G_GUINT64_FORMAT, + segment->start); + + /* + According to RFC7233, the range is inclusive: + The first-byte-pos value in a byte-range-spec gives the byte-offset + of the first byte in a range. The last-byte-pos value gives the + byte-offset of the last byte in the range; that is, the byte + positions specified are inclusive. Byte offsets start at zero. + */ + + g_mutex_lock (&src->mutex); + if (!src->uri) { + GST_DEBUG ("attempt to seek before URI set"); + g_mutex_unlock (&src->mutex); + return FALSE; + } + if (src->input.status_code >= 200 && src->input.status_code < 300) { + if (segment->start >= src->input.size) { + GST_DEBUG ("attempt to seek to %" G_GUINT64_FORMAT " but size is %" + G_GUINT64_FORMAT, segment->start, src->input.size); + g_mutex_unlock (&src->mutex); + return FALSE; + } + if (segment->stop != -1 && segment->stop > src->input.size) { + g_mutex_unlock (&src->mutex); + return FALSE; + } + } else { + GST_DEBUG ("Attempt to seek on a URL that will generate HTTP error %u", + src->input.status_code); + } + src->position = segment->start; + + if (segment->stop != -1) { + src->segment_end = segment->stop; + } else { + src->segment_end = src->input.size; + } + g_mutex_unlock (&src->mutex); + return TRUE; +} + +static gboolean +gst_test_http_src_get_size (GstBaseSrc * basesrc, guint64 * size) +{ + GstTestHTTPSrc *src; + + src = GST_TEST_HTTP_SRC (basesrc); + + g_mutex_lock (&src->mutex); + /* if it was started, size is set */ + if (src->uri && src->input.status_code >= 200 && src->input.status_code < 300) { + *size = src->input.size; + g_mutex_unlock (&src->mutex); + return TRUE; + } + /* cannot get the size if it wasn't started */ + g_mutex_unlock (&src->mutex); + return FALSE; +} + +static GstFlowReturn +gst_test_http_src_create (GstBaseSrc * basesrc, guint64 offset, + guint length, GstBuffer ** retbuf) +{ + GstTestHTTPSrc *src = GST_TEST_HTTP_SRC (basesrc); + guint bytes_read; + GstFlowReturn ret = GST_FLOW_OK; + guint blocksize; + + fail_unless (gst_test_http_src_callbacks != NULL); + fail_unless (gst_test_http_src_callbacks->src_create != NULL); + + GST_OBJECT_LOCK (src); + blocksize = basesrc->blocksize; + GST_OBJECT_UNLOCK (src); + + g_mutex_lock (&src->mutex); + GST_DEBUG ("gst_test_http_src_create feeding from %" G_GUINT64_FORMAT, + src->position); + if (src->uri == NULL) { + GST_ELEMENT_ERROR (src, RESOURCE, READ, (NULL), GST_ERROR_SYSTEM); + g_mutex_unlock (&src->mutex); + return GST_FLOW_ERROR; + } + if (src->input.status_code < 200 || src->input.status_code >= 300) { + GST_ELEMENT_ERROR (src, RESOURCE, NOT_FOUND, ("%s", + "Generated requested error"), ("%s (%d), URL: %s, Redirect to: %s", + "Generated requested error", src->input.status_code, src->uri, + GST_STR_NULL (NULL))); + g_mutex_unlock (&src->mutex); + return GST_FLOW_ERROR; + } + if (src->http_method == METHOD_INVALID) { + GST_ELEMENT_ERROR (src, RESOURCE, READ, ("%s", + "Invalid HTTP method"), ("%s (%s), URL: %s", + "Invalid HTTP method", src->http_method_name, src->uri)); + g_mutex_unlock (&src->mutex); + return GST_FLOW_ERROR; + } else if (src->http_method == METHOD_HEAD) { + ret = GST_FLOW_EOS; + goto http_events; + } + fail_unless_equals_uint64 (offset, src->position); + bytes_read = MIN ((src->segment_end - src->position), blocksize); + if (bytes_read == 0) { + ret = GST_FLOW_EOS; + goto http_events; + } + ret = gst_test_http_src_callbacks->src_create (src, + offset, bytes_read, retbuf, + src->input.context, gst_test_http_src_callback_user_data); + if (ret != GST_FLOW_OK) { + goto http_events; + } + + GST_BUFFER_OFFSET (*retbuf) = src->position; + GST_BUFFER_OFFSET_END (*retbuf) = src->position + bytes_read; + + src->position += bytes_read; +http_events: + if (src->http_headers_event) { + gst_pad_push_event (GST_BASE_SRC_PAD (src), src->http_headers_event); + src->http_headers_event = NULL; + } + if (src->duration_changed) { + src->duration_changed = FALSE; + gst_element_post_message (GST_ELEMENT (src), + gst_message_new_duration_changed (GST_OBJECT (src))); + } + + g_mutex_unlock (&src->mutex); + return ret; +} + +static void +gst_test_http_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstTestHTTPSrc *src = GST_TEST_HTTP_SRC (object); + + switch (prop_id) { + case PROP_USER_AGENT: + g_free (src->user_agent); + src->user_agent = g_value_dup_string (value); + break; + case PROP_EXTRA_HEADERS:{ + const GstStructure *s = gst_value_get_structure (value); + if (src->extra_headers) + gst_structure_free (src->extra_headers); + src->extra_headers = s ? gst_structure_copy (s) : NULL; + break; + } + case PROP_COMPRESS: + src->compress = g_value_get_boolean (value); + GST_DEBUG ("Set compress=%s", src->compress ? "TRUE" : "FALSE"); + break; + case PROP_KEEP_ALIVE: + src->keep_alive = g_value_get_boolean (value); + break; + case PROP_METHOD: + g_free (src->http_method_name); + src->http_method_name = g_value_dup_string (value); + src->http_method = METHOD_INVALID; + for (guint i = 0; gst_test_http_src_methods[i].name; ++i) { + if (strcmp (gst_test_http_src_methods[i].name, + src->http_method_name) == 0) { + src->http_method = gst_test_http_src_methods[i].method; + break; + } + } + /* we don't cause an error for an invalid method at this point, + as GstSoupHTTPSrc does not use the http_method_name string until + trying to open a connection. + */ + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_test_http_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstTestHTTPSrc *src = GST_TEST_HTTP_SRC (object); + + switch (prop_id) { + case PROP_USER_AGENT: + g_value_set_string (value, src->user_agent); + break; + case PROP_EXTRA_HEADERS: + gst_value_set_structure (value, src->extra_headers); + break; + case PROP_COMPRESS: + g_value_set_boolean (value, src->compress); + break; + case PROP_KEEP_ALIVE: + g_value_set_boolean (value, src->keep_alive); + break; + case PROP_METHOD: + g_value_set_string (value, src->http_method_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +gst_test_http_src_set_location (GstTestHTTPSrc * src, + const gchar * uri, GError ** error) +{ + g_mutex_lock (&src->mutex); + g_free (src->uri); + src->uri = g_strdup (uri); + g_mutex_unlock (&src->mutex); + return TRUE; +} + +static GstURIType +gst_test_http_src_uri_get_type (GType type) +{ + return GST_URI_SRC; +} + +static const gchar *const * +gst_test_http_src_uri_get_protocols (GType type) +{ + static const gchar *protocols[] = { "http", NULL }; + + return protocols; +} + +static gchar * +gst_test_http_src_uri_get_uri (GstURIHandler * handler) +{ + GstTestHTTPSrc *src = GST_TEST_HTTP_SRC (handler); + gchar *ret; + g_mutex_lock (&src->mutex); + ret = g_strdup (src->uri); + g_mutex_unlock (&src->mutex); + return ret; +} + +static gboolean +gst_test_http_src_uri_set_uri (GstURIHandler * handler, const gchar * uri, + GError ** err) +{ + GstTestHTTPSrc *src = GST_TEST_HTTP_SRC (handler); + + return gst_test_http_src_set_location (src, uri, err); +} + +static void +gst_test_http_src_uri_handler_init (gpointer g_iface, gpointer iface_data) +{ + GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface; + + iface->get_type = gst_test_http_src_uri_get_type; + iface->get_protocols = gst_test_http_src_uri_get_protocols; + iface->get_uri = gst_test_http_src_uri_get_uri; + iface->set_uri = gst_test_http_src_uri_set_uri; +} + +static gboolean +gst_test_http_src_plugin_init_func (GstPlugin * plugin, gpointer user_data) +{ + PluginInitContext *context = (PluginInitContext *) user_data; + gboolean ret; + + ret = + gst_element_register (plugin, context->name, context->rank, + context->type); + return ret; +} + +gboolean +gst_test_http_src_register_plugin (GstRegistry * registry, const gchar * name) +{ + gboolean ret; + PluginInitContext context; + + context.name = name; + context.rank = GST_RANK_PRIMARY + 1; + context.type = GST_TYPE_TEST_HTTP_SRC; + ret = gst_plugin_register_static_full (GST_VERSION_MAJOR, /* version */ + GST_VERSION_MINOR, /* version */ + name, /* name */ + "Replaces a souphttpsrc plugin and returns predefined data.", /* description */ + gst_test_http_src_plugin_init_func, /* init function */ + "0.0.0", /* version string */ + GST_LICENSE_UNKNOWN, /* license */ + __FILE__, /* source */ + GST_PACKAGE_NAME, /* package */ + GST_PACKAGE_ORIGIN, /* origin */ + &context /* user_data */ + ); + return ret; +} + +void +gst_test_http_src_install_callbacks (const GstTestHTTPSrcCallbacks * + callbacks, gpointer user_data) +{ + gst_test_http_src_callbacks = callbacks; + gst_test_http_src_callback_user_data = user_data; +} + +void +gst_test_http_src_set_default_blocksize (guint blocksize) +{ + gst_test_http_src_blocksize = blocksize; +} diff --git a/tests/check/elements/test_http_src.h b/tests/check/elements/test_http_src.h new file mode 100644 index 0000000000..61161afb4d --- /dev/null +++ b/tests/check/elements/test_http_src.h @@ -0,0 +1,132 @@ +/* HTTP source element for use in tests + * + * Copyright (c) <2015> YouView TV Ltd + * + * 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. + */ + +#ifndef __GST_TEST_HTTP_SRC_H__ +#define __GST_TEST_HTTP_SRC_H__ + +#include + +G_BEGIN_DECLS + +#define GST_TYPE_TEST_HTTP_SRC (gst_test_http_src_get_type ()) + +/* structure used by src_start function to configure the + * GstTestHTTPSrc plugin. + * It specifies information about a given URI. + */ +typedef struct _GstTestHTTPSrcInput +{ + gpointer context; /* opaque pointer that can be used in callbacks */ + guint64 size; /* size of resource, in bytes */ + GstStructure *request_headers; + GstStructure *response_headers; + guint status_code; /* HTTP status code */ +} GstTestHTTPSrcInput; + +/* Opaque structure used by GstTestHTTPSrc */ +typedef struct _GstTestHTTPSrc GstTestHTTPSrc; + +typedef struct _GstTestHTTPSrcCallbacks { + /** + * src_start: + * @src: The #GstTestHTTPSrc calling this callback + * @uri: The URI that is being requested + * @input_data: (out) The implementation of this callback is + * responsible for filling in this #GstTestHTTPSrcInput + * with the appropriate information, return returning %TRUE. + * If returning %FALSE, only GstTestHTTPSrcInput::status_code + * should be updated. + * Returns: %TRUE if GstTestHTTPSrc should respond to this URI, + * using the supplied input_data. + * + * src_start is used to "open" the given URI. The callback must return + * %TRUE to simulate a success, and set appropriate fields in input_data. + * Returning %FALSE indicates that the request URI is not found. + * In this situation GstTestHTTPSrc will cause the appropriate + * 404 error to be posted to the bus + */ + gboolean (*src_start)(GstTestHTTPSrc *src, + const gchar *uri, + GstTestHTTPSrcInput *input_data, + gpointer user_data); + /** + * src_create: + * @src: the #GstTestHTTPSrc calling this callback + * @offset: the offset from the start of the resource + * @length: requested number of bytes + * @retbuf: (out) used to return a newly allocated #GstBuffer + * @context: (allow none) the value of the context field + * in #GstTestHTTPSrcInput. + * @user_data: the value of user_data provided to + * #gst_test_http_src_install_callbacks + * Returns: %GST_FLOW_OK to indicate success, or some other value of + * #GstFlowReturn to indicate EOS or error. + * + * The src_create function is used to create a #GstBuffer for + * simulating the data that is returned when accessing this + * "open" stream. It can also be used to simulate various error + * conditions by returning something other than %GST_FLOW_OK + */ + GstFlowReturn (*src_create)(GstTestHTTPSrc *src, + guint64 offset, + guint length, + GstBuffer ** retbuf, + gpointer context, + gpointer user_data); +} GstTestHTTPSrcCallbacks; + +GType gst_test_http_src_get_type (void); + +/** + * gst_test_http_src_register_plugin: + * @registry: the #GstRegistry to use for registering this plugin + * @name: the name to use for this plugin + * Returns: true if successful + * + * Registers this plugin with the GstRegitry using the given name. It will + * be given a high rank, so that it will be picked in preference to any + * other element that implements #GstURIHandler. + */ +gboolean gst_test_http_src_register_plugin (GstRegistry * registry, const gchar * name); + +/** + * gst_test_http_src_install_callbacks: + * @callbacks: the #GstTestHTTPSrcCallbacks callback functions that will + * be called every time this element is asked to open a URI or provide data + * for an open URI. + * @user_data: a pointer that is passed to every callback + */ +void gst_test_http_src_install_callbacks (const GstTestHTTPSrcCallbacks *callbacks, gpointer user_data); + +/** + * gst_test_http_src_set_default_blocksize: + * @blocksize: the default block size to use (0=use #GstBaseSrc default) + * + * Set the default blocksize that will be used by instances of + * #GstTestHTTPSrc. It specifies the size (in bytes) that will be + * returned in each #GstBuffer. This default can be overridden + * by an instance of #GstTestHTTPSrc using the "blocksize" property + * of #GstBaseSrc + */ +void gst_test_http_src_set_default_blocksize (guint blocksize); + +G_END_DECLS + +#endif /* __GST_TEST_HTTP_SRC_H__ */