gstreamer/tests/check/elements/hls_demux.c

603 lines
20 KiB
C
Raw Normal View History

/* GStreamer unit test for HLS demux
*
* 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 <gst/check/gstcheck.h>
#include "adaptive_demux_common.h"
#define DEMUX_ELEMENT_NAME "hlsdemux"
#define TS_PACKET_LEN 188
typedef struct _GstHlsDemuxTestInputData
{
const gchar *uri;
const guint8 *payload;
guint64 size;
} GstHlsDemuxTestInputData;
typedef struct _GstHlsDemuxTestCase
{
const GstHlsDemuxTestInputData *input;
GstStructure *state;
} GstHlsDemuxTestCase;
typedef struct _GstHlsDemuxTestAppendUriContext
{
GQuark field_id;
const gchar *uri;
} GstHlsDemuxTestAppendUriContext;
typedef struct _GstHlsDemuxTestSelectBitrateContext
{
GstAdaptiveDemuxTestEngine *engine;
GstAdaptiveDemuxTestCase *testData;
guint select_count;
gulong signal_handle;
} GstHlsDemuxTestSelectBitrateContext;
static GByteArray *
generate_transport_stream (guint length)
{
guint pos;
guint cc = 0;
GByteArray *mpeg_ts;
fail_unless ((length % TS_PACKET_LEN) == 0);
mpeg_ts = g_byte_array_sized_new (length);
if (!mpeg_ts) {
return NULL;
}
memset (mpeg_ts->data, 0xFF, length);
for (pos = 0; pos < length; pos += TS_PACKET_LEN) {
mpeg_ts->data[pos] = 0x47;
mpeg_ts->data[pos + 1] = 0x1F;
mpeg_ts->data[pos + 2] = 0xFF;
mpeg_ts->data[pos + 3] = cc;
cc = (cc + 1) & 0x0F;
}
return mpeg_ts;
}
static GByteArray *
setup_test_variables (GstHlsDemuxTestInputData * inputTestData,
GstAdaptiveDemuxTestExpectedOutput * outputTestData,
GstHlsDemuxTestCase * hlsTestCase,
GstAdaptiveDemuxTestCase * engineTestData, guint segment_size)
{
GByteArray *mpeg_ts = NULL;
if (segment_size) {
mpeg_ts = generate_transport_stream ((segment_size));
fail_unless (mpeg_ts != NULL);
for (guint itd = 0; inputTestData[itd].uri; ++itd) {
if (g_str_has_suffix (inputTestData[itd].uri, ".ts")) {
inputTestData[itd].payload = mpeg_ts->data;
}
}
for (guint otd = 0; outputTestData[otd].name; ++otd) {
outputTestData[otd].expected_data = mpeg_ts->data;
engineTestData->output_streams =
g_list_append (engineTestData->output_streams, &outputTestData[otd]);
}
}
hlsTestCase->input = inputTestData;
hlsTestCase->state = gst_structure_new_empty (__FUNCTION__);
return mpeg_ts;
}
#define TESTCASE_INIT_BOILERPLATE(segment_size) \
GstTestHTTPSrcCallbacks http_src_callbacks = { 0 }; \
GstAdaptiveDemuxTestCallbacks engine_callbacks = { 0 }; \
GstAdaptiveDemuxTestCase *engineTestData; \
GstHlsDemuxTestCase hlsTestCase = { 0 }; \
GByteArray *mpeg_ts=NULL; \
engineTestData = gst_adaptive_demux_test_case_new(); \
fail_unless (engineTestData!=NULL); \
mpeg_ts = setup_test_variables(inputTestData, outputTestData, \
&hlsTestCase, engineTestData, segment_size); \
#define TESTCASE_UNREF_BOILERPLATE do{ \
if(engineTestData->signal_context){ \
g_slice_free (GstHlsDemuxTestSelectBitrateContext, engineTestData->signal_context); \
} \
if(mpeg_ts) { g_byte_array_free (mpeg_ts, TRUE); } \
gst_structure_free (hlsTestCase.state); \
g_object_unref (engineTestData); \
} while(0)
static gboolean
append_request_uri (GQuark field_id, GValue * value, gpointer user_data)
{
GstHlsDemuxTestAppendUriContext *context =
(GstHlsDemuxTestAppendUriContext *) user_data;
GValue uri_val = G_VALUE_INIT;
if (context->field_id == field_id) {
g_value_init (&uri_val, G_TYPE_STRING);
g_value_set_string (&uri_val, context->uri);
gst_value_array_append_value (value, &uri_val);
g_value_unset (&uri_val);
}
return TRUE;
}
static void
gst_hlsdemux_test_set_input_data (const GstHlsDemuxTestCase * test_case,
const GstHlsDemuxTestInputData * input, GstTestHTTPSrcInput * output)
{
output->size = input->size;
output->context = (gpointer) input;
if (output->size == 0) {
output->size = strlen ((gchar *) input->payload);
}
fail_unless (input->uri != NULL);
if (g_str_has_suffix (input->uri, ".m3u8")) {
output->response_headers = gst_structure_new ("response-headers",
"Content-Type", G_TYPE_STRING, "application/vnd.apple.mpegurl", NULL);
} else if (g_str_has_suffix (input->uri, ".ts")) {
output->response_headers = gst_structure_new ("response-headers",
"Content-Type", G_TYPE_STRING, "video/mp2t", NULL);
}
if (gst_structure_has_field (test_case->state, "requests")) {
GstHlsDemuxTestAppendUriContext context =
{ g_quark_from_string ("requests"), input->uri };
gst_structure_map_in_place (test_case->state, append_request_uri, &context);
} else {
GValue requests = G_VALUE_INIT;
GValue uri_val = G_VALUE_INIT;
g_value_init (&requests, GST_TYPE_ARRAY);
g_value_init (&uri_val, G_TYPE_STRING);
g_value_set_string (&uri_val, input->uri);
gst_value_array_append_value (&requests, &uri_val);
gst_structure_set_value (test_case->state, "requests", &requests);
g_value_unset (&uri_val);
g_value_unset (&requests);
}
}
static gboolean
gst_hlsdemux_test_src_start (GstTestHTTPSrc * src,
const gchar * uri, GstTestHTTPSrcInput * input_data, gpointer user_data)
{
const GstHlsDemuxTestCase *test_case =
(const GstHlsDemuxTestCase *) user_data;
guint fail_count = 0;
GST_DEBUG ("src_start %s", uri);
for (guint i = 0; test_case->input[i].uri; ++i) {
if (strcmp (test_case->input[i].uri, uri) == 0) {
gst_hlsdemux_test_set_input_data (test_case, &test_case->input[i],
input_data);
GST_DEBUG ("open URI %s", uri);
return TRUE;
}
}
gst_structure_get_uint (test_case->state, "failure-count", &fail_count);
fail_count++;
gst_structure_set (test_case->state, "failure-count", G_TYPE_UINT,
fail_count, NULL);
return FALSE;
}
static GstFlowReturn
gst_hlsdemux_test_src_create (GstTestHTTPSrc * src,
guint64 offset,
guint length, GstBuffer ** retbuf, gpointer context, gpointer user_data)
{
GstBuffer *buf;
/* const GstHlsDemuxTestCase *test_case = (const GstHlsDemuxTestCase *) user_data; */
GstHlsDemuxTestInputData *input = (GstHlsDemuxTestInputData *) context;
buf = gst_buffer_new_allocate (NULL, length, NULL);
fail_if (buf == NULL, "Not enough memory to allocate buffer");
fail_if (input->payload == NULL);
gst_buffer_fill (buf, 0, input->payload + offset, length);
*retbuf = buf;
return GST_FLOW_OK;
}
static GstFlowReturn
gst_hlsdemux_test_network_error_src_create (GstTestHTTPSrc * src,
guint64 offset,
guint length, GstBuffer ** retbuf, gpointer context, gpointer user_data)
{
const GstHlsDemuxTestCase *test_case =
(const GstHlsDemuxTestCase *) user_data;
GstHlsDemuxTestInputData *input = (GstHlsDemuxTestInputData *) context;
const gchar *failure_suffix;
guint64 failure_position = 0;
fail_unless (test_case != NULL);
fail_unless (input != NULL);
fail_unless (input->uri != NULL);
failure_suffix =
gst_structure_get_string (test_case->state, "failure-suffix");
if (!failure_suffix) {
failure_suffix = ".ts";
}
if (!gst_structure_get_uint64 (test_case->state, "failure-position",
&failure_position)) {
failure_position = 10 * TS_PACKET_LEN;
}
GST_DEBUG ("network_error %s %s %" G_GUINT64_FORMAT " @ %" G_GUINT64_FORMAT,
input->uri, failure_suffix, offset, failure_position);
if (g_str_has_suffix (input->uri, failure_suffix)
&& offset >= failure_position) {
GST_DEBUG ("return error");
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."));
*retbuf = NULL;
return GST_FLOW_ERROR;
}
return gst_hlsdemux_test_src_create (src, offset, length, retbuf, context,
user_data);
}
/******************** Test specific code starts here **************************/
/*
* Test a media manifest with a single segment
*
*/
GST_START_TEST (simpleTest)
{
/* segment_size needs to larger than 2K, otherwise gsthlsdemux will
not perform a typefind on the buffer */
const guint segment_size = 30 * TS_PACKET_LEN;
const gchar *manifest =
"#EXTM3U \n"
"#EXT-X-TARGETDURATION:1\n"
"#EXTINF:1,Test\n" "001.ts\n" "#EXT-X-ENDLIST\n";
GstHlsDemuxTestInputData inputTestData[] = {
{"http://unit.test/media.m3u8", (guint8 *) manifest, 0},
{"http://unit.test/001.ts", NULL, segment_size},
{NULL, NULL, 0},
};
GstAdaptiveDemuxTestExpectedOutput outputTestData[] = {
{"src_0", segment_size, NULL},
{NULL, 0, NULL}
};
TESTCASE_INIT_BOILERPLATE (segment_size);
http_src_callbacks.src_start = gst_hlsdemux_test_src_start;
http_src_callbacks.src_create = gst_hlsdemux_test_src_create;
engine_callbacks.appsink_received_data =
gst_adaptive_demux_test_check_received_data;
engine_callbacks.appsink_eos =
gst_adaptive_demux_test_check_size_of_received_data;
gst_test_http_src_install_callbacks (&http_src_callbacks, &hlsTestCase);
gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME,
inputTestData[0].uri, &engine_callbacks, engineTestData);
TESTCASE_UNREF_BOILERPLATE;
}
GST_END_TEST;
GST_START_TEST (testMasterPlaylist)
{
const guint segment_size = 30 * TS_PACKET_LEN;
const gchar *master_playlist =
"#EXTM3U\n"
"#EXT-X-VERSION:4\n"
"#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1251135, CODECS=\"avc1.42001f mp4a.40.2\", RESOLUTION=640x352\n"
"1200.m3u8\n";
const gchar *media_playlist =
"#EXTM3U \n"
"#EXT-X-TARGETDURATION:1\n"
"#EXTINF:1,Test\n" "001.ts\n" "#EXT-X-ENDLIST\n";
GstHlsDemuxTestInputData inputTestData[] = {
{"http://unit.test/master.m3u8", (guint8 *) master_playlist, 0},
{"http://unit.test/1200.m3u8", (guint8 *) media_playlist, 0},
{"http://unit.test/001.ts", NULL, segment_size},
{NULL, NULL, 0}
};
GstAdaptiveDemuxTestExpectedOutput outputTestData[] = {
{"src_0", segment_size, NULL},
{NULL, 0, NULL}
};
const GValue *requests;
TESTCASE_INIT_BOILERPLATE (segment_size);
http_src_callbacks.src_start = gst_hlsdemux_test_src_start;
http_src_callbacks.src_create = gst_hlsdemux_test_src_create;
engine_callbacks.appsink_received_data =
gst_adaptive_demux_test_check_received_data;
engine_callbacks.appsink_eos =
gst_adaptive_demux_test_check_size_of_received_data;
gst_test_http_src_install_callbacks (&http_src_callbacks, &hlsTestCase);
gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME,
"http://unit.test/master.m3u8", &engine_callbacks, engineTestData);
requests = gst_structure_get_value (hlsTestCase.state, "requests");
fail_unless (requests != NULL);
assert_equals_uint64 (gst_value_array_get_size (requests),
sizeof (inputTestData) / sizeof (inputTestData[0]) - 1);
for (guint i = 0; inputTestData[i].uri; ++i) {
const GValue *uri;
uri = gst_value_array_get_value (requests, i);
fail_unless (uri != NULL);
assert_equals_string (inputTestData[i].uri, g_value_get_string (uri));
}
TESTCASE_UNREF_BOILERPLATE;
}
GST_END_TEST;
/*
* Test seeking
*
*/
GST_START_TEST (testSeek)
{
const guint segment_size = 60 * TS_PACKET_LEN;
const gchar *manifest =
"#EXTM3U \n"
"#EXT-X-TARGETDURATION:1\n"
"#EXTINF:1,Test\n" "001.ts\n" "#EXT-X-ENDLIST\n";
GstHlsDemuxTestInputData inputTestData[] = {
{"http://unit.test/media.m3u8", (guint8 *) manifest, 0},
{"http://unit.test/001.ts", NULL, segment_size},
{NULL, NULL, 0},
};
GstAdaptiveDemuxTestExpectedOutput outputTestData[] = {
{"src_0", segment_size, NULL},
{NULL, 0, NULL}
};
GstTestHTTPSrcCallbacks http_src_callbacks = { 0 };
GstAdaptiveDemuxTestCase *engineTestData;
GstHlsDemuxTestCase hlsTestCase = { 0 };
GByteArray *mpeg_ts = NULL;
engineTestData = gst_adaptive_demux_test_case_new ();
mpeg_ts = setup_test_variables (inputTestData, outputTestData,
&hlsTestCase, engineTestData, segment_size);
http_src_callbacks.src_start = gst_hlsdemux_test_src_start;
http_src_callbacks.src_create = gst_hlsdemux_test_src_create;
/* seek to 5ms.
* Because there is only one fragment, we expect the whole file to be
* downloaded again
*/
engineTestData->threshold_for_seek = 20 * TS_PACKET_LEN;
engineTestData->seek_event =
gst_event_new_seek (1.0, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, GST_SEEK_TYPE_SET,
5 * GST_MSECOND, GST_SEEK_TYPE_NONE, 0);
gst_test_http_src_install_callbacks (&http_src_callbacks, &hlsTestCase);
gst_adaptive_demux_test_seek (DEMUX_ELEMENT_NAME,
inputTestData[0].uri, engineTestData);
TESTCASE_UNREF_BOILERPLATE;
}
GST_END_TEST;
static void
testDownloadErrorMessageCallback (GstAdaptiveDemuxTestEngine * engine,
GstMessage * msg, gpointer user_data)
{
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);
g_error_free (err);
g_free (dbg_info);
g_main_loop_quit (engine->loop);
}
/* test failing to download the media playlist */
GST_START_TEST (testMediaPlaylistNotFound)
{
const gchar *master_playlist =
"#EXTM3U\n"
"#EXT-X-VERSION:4\n"
"#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1251135, CODECS=\"avc1.42001f mp4a.40.2\", RESOLUTION=640x352\n"
"1200.m3u8\n";
GstHlsDemuxTestInputData inputTestData[] = {
{"http://unit.test/master.m3u8", (guint8 *) master_playlist, 0},
{NULL, NULL, 0}
};
GstAdaptiveDemuxTestExpectedOutput outputTestData[] = {
{"src_0", 0, NULL},
{NULL, 0, NULL}
};
TESTCASE_INIT_BOILERPLATE (0);
gst_structure_set (hlsTestCase.state,
"failure-count", G_TYPE_UINT, 0,
"failure-suffix", G_TYPE_STRING, "1200.m3u8", NULL);
http_src_callbacks.src_start = gst_hlsdemux_test_src_start;
http_src_callbacks.src_create = gst_hlsdemux_test_src_create;
engine_callbacks.appsink_received_data =
gst_adaptive_demux_test_check_received_data;
engine_callbacks.bus_error_message = testDownloadErrorMessageCallback;
gst_test_http_src_install_callbacks (&http_src_callbacks, &hlsTestCase);
gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME,
"http://unit.test/master.m3u8", &engine_callbacks, engineTestData);
TESTCASE_UNREF_BOILERPLATE;
}
GST_END_TEST;
static void
hlsdemux_test_check_no_data_received (GstAdaptiveDemuxTestEngine
* engine, GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data)
{
assert_equals_uint64 (stream->total_received_size, 0);
g_main_loop_quit (engine->loop);
}
/* test failing to download a media segment (a 404 error) */
GST_START_TEST (testFragmentNotFound)
{
const gchar *master_playlist =
"#EXTM3U\n"
"#EXT-X-VERSION:4\n"
"#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1251135, CODECS=\"avc1.42001f mp4a.40.2\", RESOLUTION=640x352\n"
"1200.m3u8\n";
const gchar *media_playlist =
"#EXTM3U \n"
"#EXT-X-TARGETDURATION:1\n"
"#EXTINF:1,Test\n" "001.ts\n" "#EXT-X-ENDLIST\n";
GstHlsDemuxTestInputData inputTestData[] = {
{"http://unit.test/master.m3u8", (guint8 *) master_playlist, 0},
{"http://unit.test/1200.m3u8", (guint8 *) media_playlist, 0},
{NULL, NULL, 0}
};
GstAdaptiveDemuxTestExpectedOutput outputTestData[] = {
{"src_0", 0, NULL},
{NULL, 0, NULL}
};
TESTCASE_INIT_BOILERPLATE (0);
gst_structure_set (hlsTestCase.state,
"failure-count", G_TYPE_UINT, 0,
"failure-suffix", G_TYPE_STRING, "001.ts", NULL);
http_src_callbacks.src_start = gst_hlsdemux_test_src_start;
http_src_callbacks.src_create = gst_hlsdemux_test_src_create;
engine_callbacks.appsink_received_data =
gst_adaptive_demux_test_check_received_data;
engine_callbacks.appsink_eos = hlsdemux_test_check_no_data_received;
engine_callbacks.bus_error_message = testDownloadErrorMessageCallback;
gst_test_http_src_install_callbacks (&http_src_callbacks, &hlsTestCase);
gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME,
"http://unit.test/master.m3u8", &engine_callbacks, engineTestData);
TESTCASE_UNREF_BOILERPLATE;
}
GST_END_TEST;
/* work-around that adaptivedemux is not posting an error message
about failure to download a fragment */
static void
missing_message_eos_callback (GstAdaptiveDemuxTestEngine * engine,
GstAdaptiveDemuxTestOutputStream * stream, gpointer user_data)
{
GstAdaptiveDemuxTestCase *testData = GST_ADAPTIVE_DEMUX_TEST_CASE (user_data);
GstAdaptiveDemuxTestExpectedOutput *testOutputStreamData;
fail_unless (stream != NULL);
testOutputStreamData =
gst_adaptive_demux_test_find_test_data_by_stream (testData, stream, NULL);
fail_unless (testOutputStreamData != NULL);
/* 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);
testData->count_of_finished_streams++;
GST_DEBUG ("EOS callback %d %d",
testData->count_of_finished_streams,
g_list_length (testData->output_streams));
if (testData->count_of_finished_streams ==
g_list_length (testData->output_streams)) {
g_main_loop_quit (engine->loop);
}
}
/*
* Test fragment download error
* Let the adaptive demux download a few bytes, then instruct the
* test soup http src element to generate an error.
*/
GST_START_TEST (testFragmentDownloadError)
{
const guint segment_size = 30 * TS_PACKET_LEN;
const gchar *master_playlist =
"#EXTM3U\n"
"#EXT-X-VERSION:4\n"
"#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1251135, CODECS=\"avc1.42001f mp4a.40.2\", RESOLUTION=640x352\n"
"1200.m3u8\n";
const gchar *media_playlist =
"#EXTM3U \n"
"#EXT-X-VERSION:4\n"
"#EXT-X-TARGETDURATION:1\n"
"#EXTINF:1,Test\n" "001.ts\n"
"#EXTINF:1,Test\n" "002.ts\n" "#EXT-X-ENDLIST\n";
GstHlsDemuxTestInputData inputTestData[] = {
{"http://unit.test/master.m3u8", (guint8 *) master_playlist, 0},
{"http://unit.test/1200.m3u8", (guint8 *) media_playlist, 0},
{"http://unit.test/001.ts", NULL, segment_size},
{"http://unit.test/002.ts", NULL, segment_size},
{NULL, NULL, 0}
};
GstAdaptiveDemuxTestExpectedOutput outputTestData[] = {
{"src_0", 2 * segment_size, NULL},
{NULL, 0, NULL}
};
const guint64 failure_position = 2048;
TESTCASE_INIT_BOILERPLATE (segment_size);
http_src_callbacks.src_start = gst_hlsdemux_test_src_start;
http_src_callbacks.src_create = gst_hlsdemux_test_network_error_src_create;
gst_structure_set (hlsTestCase.state,
"failure-suffix", G_TYPE_STRING, "001.ts",
"failure-position", G_TYPE_UINT64, failure_position, NULL);
engine_callbacks.appsink_received_data =
gst_adaptive_demux_test_check_received_data;
engine_callbacks.appsink_eos = missing_message_eos_callback;
engine_callbacks.bus_error_message = testDownloadErrorMessageCallback;
gst_test_http_src_install_callbacks (&http_src_callbacks, &hlsTestCase);
gst_adaptive_demux_test_run (DEMUX_ELEMENT_NAME,
inputTestData[0].uri, &engine_callbacks, engineTestData);
TESTCASE_UNREF_BOILERPLATE;
}
GST_END_TEST;
static Suite *
hls_demux_suite (void)
{
Suite *s = suite_create ("hls_demux");
TCase *tc_basicTest = tcase_create ("basicTest");
tcase_add_test (tc_basicTest, simpleTest);
tcase_add_test (tc_basicTest, testMasterPlaylist);
tcase_add_test (tc_basicTest, testMediaPlaylistNotFound);
tcase_add_test (tc_basicTest, testFragmentNotFound);
tcase_add_test (tc_basicTest, testFragmentDownloadError);
tcase_add_test (tc_basicTest, testSeek);
tcase_add_unchecked_fixture (tc_basicTest, gst_adaptive_demux_test_setup,
gst_adaptive_demux_test_teardown);
suite_add_tcase (s, tc_basicTest);
return s;
}
GST_CHECK_MAIN (hls_demux);