From 35f966cf13e9dac6d6e3ba2211f3683489a28ccc Mon Sep 17 00:00:00 2001 From: Andy Wingo Date: Mon, 30 Jan 2006 17:01:54 +0000 Subject: [PATCH] ext/theora/: Same changes as were done to vorbisenc, although theoraenc was timestamping correctly. Added handling of... Original commit message from CVS: 2006-01-30 Andy Wingo * ext/theora/gsttheoraenc.h: * ext/theora/theoraenc.c: Same changes as were done to vorbisenc, although theoraenc was timestamping correctly. Added handling of streams that start with nonzero timestamps. * tests/check/Makefile.am: * tests/check/pipelines/theoraenc.c: New file, basically does same tests as vorbisenc. * tests/check/pipelines/vorbisenc.c: I claim these bugs. --- ChangeLog | 13 ++ ext/theora/gsttheoraenc.h | 6 +- ext/theora/theoraenc.c | 52 ++++-- tests/check/Makefile.am | 7 + tests/check/pipelines/theoraenc.c | 360 ++++++++++++++++++++++++++++++++++++++ tests/check/pipelines/vorbisenc.c | 2 +- 6 files changed, 421 insertions(+), 19 deletions(-) create mode 100644 tests/check/pipelines/theoraenc.c diff --git a/ChangeLog b/ChangeLog index 4f3a51c..359ac2f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2006-01-30 Andy Wingo + + * ext/theora/gsttheoraenc.h: + * ext/theora/theoraenc.c: Same changes as were done to vorbisenc, + although theoraenc was timestamping correctly. Added handling of + streams that start with nonzero timestamps. + + * tests/check/Makefile.am: + * tests/check/pipelines/theoraenc.c: New file, basically does same + tests as vorbisenc. + + * tests/check/pipelines/vorbisenc.c: I claim these bugs. + 2006-01-30 Wim Taymans * gst-libs/gst/audio/gstaudiosink.c: diff --git a/ext/theora/gsttheoraenc.h b/ext/theora/gsttheoraenc.h index 2dd113a..55beb89 100644 --- a/ext/theora/gsttheoraenc.h +++ b/ext/theora/gsttheoraenc.h @@ -77,11 +77,13 @@ struct _GstTheoraEnc gint info_width, info_height; gint width, height; gint offset_x, offset_y; - gdouble fps; + gint fps_n, fps_d; + GstClockTime next_ts; guint packetno; guint64 bytes_out; - guint64 initial_delay; + guint64 granulepos_offset; + guint64 timestamp_offset; }; struct _GstTheoraEncClass diff --git a/ext/theora/theoraenc.c b/ext/theora/theoraenc.c index 2edea06..3f7c121 100644 --- a/ext/theora/theoraenc.c +++ b/ext/theora/theoraenc.c @@ -271,8 +271,8 @@ theora_enc_sink_setcaps (GstPad * pad, GstCaps * caps) enc->info.offset_x = enc->offset_x; enc->info.offset_y = enc->offset_y; - enc->info.fps_numerator = fps_n; - enc->info.fps_denominator = fps_d; + enc->info.fps_numerator = enc->fps_n = fps_n; + enc->info.fps_denominator = enc->fps_d = fps_d; if (par) { enc->info.aspect_numerator = gst_value_get_fraction_numerator (par); enc->info.aspect_denominator = gst_value_get_fraction_denominator (par); @@ -318,8 +318,8 @@ theora_buffer_from_packet (GstTheoraEnc * enc, ogg_packet * packet, memcpy (GST_BUFFER_DATA (buf), packet->packet, packet->bytes); GST_BUFFER_OFFSET (buf) = enc->bytes_out; - GST_BUFFER_OFFSET_END (buf) = packet->granulepos; - GST_BUFFER_TIMESTAMP (buf) = timestamp; + GST_BUFFER_OFFSET_END (buf) = packet->granulepos + enc->granulepos_offset; + GST_BUFFER_TIMESTAMP (buf) = timestamp + enc->timestamp_offset; GST_BUFFER_DURATION (buf) = duration; /* the second most significant bit of the first data byte is cleared @@ -417,10 +417,12 @@ theora_enc_sink_event (GstPad * pad, GstEvent * event) case GST_EVENT_EOS: /* push last packet with eos flag */ while (theora_encode_packetout (&enc->state, 1, &op)) { - GstClockTime out_time = - theora_granule_time (&enc->state, op.granulepos) * GST_SECOND; + /* See comment in the chain function */ + GstClockTime next_time = + theora_granule_time (&enc->state, op.granulepos + 1) * GST_SECOND; - theora_push_packet (enc, &op, out_time, GST_SECOND / enc->fps); + theora_push_packet (enc, &op, enc->next_ts, next_time - enc->next_ts); + enc->next_ts = next_time; } res = gst_pad_push_event (enc->srcpad, event); break; @@ -447,6 +449,9 @@ theora_enc_chain (GstPad * pad, GstBuffer * buffer) GstCaps *caps; GstBuffer *buf1, *buf2, *buf3; + enc->granulepos_offset = 0; + enc->timestamp_offset = 0; + /* Theora streams begin with three headers; the initial header (with most of the codec setup parameters) which is mandated by the Ogg bitstream spec. The second header holds any comment fields. The @@ -456,7 +461,8 @@ theora_enc_chain (GstPad * pad, GstBuffer * buffer) /* first packet will get its own page automatically */ theora_encode_header (&enc->state, &op); - ret = theora_buffer_from_packet (enc, &op, 0, 0, &buf1); + ret = theora_buffer_from_packet (enc, &op, GST_CLOCK_TIME_NONE, + GST_CLOCK_TIME_NONE, &buf1); if (ret != GST_FLOW_OK) { goto header_buffer_alloc; } @@ -467,14 +473,16 @@ theora_enc_chain (GstPad * pad, GstBuffer * buffer) * portably work around it. Leaks ~50 bytes per encoder instance, so not a * huge problem. */ theora_encode_comment (&enc->comment, &op); - ret = theora_buffer_from_packet (enc, &op, 0, 0, &buf2); + ret = theora_buffer_from_packet (enc, &op, GST_CLOCK_TIME_NONE, + GST_CLOCK_TIME_NONE, &buf2); if (ret != GST_FLOW_OK) { gst_buffer_unref (buf1); goto header_buffer_alloc; } theora_encode_tables (&enc->state, &op); - ret = theora_buffer_from_packet (enc, &op, 0, 0, &buf3); + ret = theora_buffer_from_packet (enc, &op, GST_CLOCK_TIME_NONE, + GST_CLOCK_TIME_NONE, &buf3); if (ret != GST_FLOW_OK) { gst_buffer_unref (buf1); gst_buffer_unref (buf2); @@ -504,6 +512,12 @@ theora_enc_chain (GstPad * pad, GstBuffer * buffer) if ((ret = theora_push_buffer (enc, buf3)) != GST_FLOW_OK) { goto header_push; } + + enc->granulepos_offset = + gst_util_uint64_scale (GST_BUFFER_TIMESTAMP (buffer), enc->fps_n, + GST_SECOND * enc->fps_d); + enc->timestamp_offset = GST_BUFFER_TIMESTAMP (buffer); + enc->next_ts = 0; } { @@ -655,11 +669,18 @@ theora_enc_chain (GstPad * pad, GstBuffer * buffer) ret = GST_FLOW_OK; while (theora_encode_packetout (&enc->state, 0, &op)) { - GstClockTime out_time; - - out_time = theora_granule_time (&enc->state, op.granulepos) * GST_SECOND; - if ((ret = theora_push_packet (enc, &op, out_time, GST_SECOND / enc->fps)) - != GST_FLOW_OK) + /* This is where we hack around theora's broken idea of what granulepos + is -- normally we wouldn't need to add the 1, because granulepos + should be the presentation time of the last sample in the packet, but + theora starts with 0 instead of 1... */ + GstClockTime next_time; + + next_time = + theora_granule_time (&enc->state, op.granulepos + 1) * GST_SECOND; + ret = + theora_push_packet (enc, &op, enc->next_ts, next_time - enc->next_ts); + enc->next_ts = next_time; + if (ret != GST_FLOW_OK) goto data_push; } gst_buffer_unref (buffer); @@ -705,7 +726,6 @@ theora_enc_change_state (GstElement * element, GstStateChange transition) theora_info_init (&enc->info); theora_comment_init (&enc->comment); enc->packetno = 0; - enc->initial_delay = 0; break; case GST_STATE_CHANGE_PAUSED_TO_PLAYING: break; diff --git a/tests/check/Makefile.am b/tests/check/Makefile.am index 6c3ab24..89cc421 100644 --- a/tests/check/Makefile.am +++ b/tests/check/Makefile.am @@ -27,7 +27,14 @@ else check_vorbis = endif +if USE_THEORA +check_theora = pipelines/theoraenc +else +check_theora = +endif + check_PROGRAMS = $(check_vorbis) \ + $(check_theora) \ elements/audioconvert \ elements/audioresample \ elements/audiotestsrc \ diff --git a/tests/check/pipelines/theoraenc.c b/tests/check/pipelines/theoraenc.c new file mode 100644 index 0000000..c0888b6 --- /dev/null +++ b/tests/check/pipelines/theoraenc.c @@ -0,0 +1,360 @@ +/* GStreamer + * + * unit test for theoraenc + * + * Copyright (C) 2006 Andy Wingo + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include + +#define TIMESTAMP_OFFSET G_GUINT64_CONSTANT(3249870963) +#define FRAMERATE 10 + +static GCond *cond = NULL; +static GMutex *lock = NULL; +static GstBuffer *buf = NULL; +static gulong id; + +static gboolean +buffer_probe (GstPad * pad, GstBuffer * buffer, gpointer unused) +{ + g_mutex_lock (lock); + + while (buf != NULL) + g_cond_wait (cond, lock); + + buf = gst_buffer_ref (buffer); + + g_cond_signal (cond); + + g_mutex_unlock (lock); + + return TRUE; +} + +static void +start_pipeline (GstElement * bin, GstPad * pad) +{ + id = gst_pad_add_buffer_probe (pad, G_CALLBACK (buffer_probe), NULL); + + cond = g_cond_new (); + lock = g_mutex_new (); + + gst_element_set_state (bin, GST_STATE_PLAYING); + +} + +static GstBuffer * +get_buffer (GstElement * bin, GstPad * pad) +{ + GstBuffer *ret; + + g_mutex_lock (lock); + + while (buf == NULL) + g_cond_wait (cond, lock); + + ret = buf; + buf = NULL; + + g_cond_signal (cond); + + g_mutex_unlock (lock); + + return ret; +} + +static void +stop_pipeline (GstElement * bin, GstPad * pad) +{ + g_mutex_lock (lock); + if (buf) + gst_buffer_unref (buf); + buf = NULL; + gst_pad_remove_buffer_probe (pad, (guint) id); + id = 0; + g_cond_signal (cond); + g_mutex_unlock (lock); + + gst_element_set_state (bin, GST_STATE_NULL); + + g_mutex_lock (lock); + if (buf) + gst_buffer_unref (buf); + buf = NULL; + g_mutex_unlock (lock); + + g_mutex_free (lock); + g_cond_free (cond); + + lock = NULL; + cond = NULL; +} + +static void +check_buffer_timestamp (GstBuffer * buffer, GstClockTime timestamp) +{ + fail_unless (GST_BUFFER_TIMESTAMP (buffer) == timestamp, + "expected timestamp %" GST_TIME_FORMAT + ", but got timestamp %" GST_TIME_FORMAT, + GST_TIME_ARGS (timestamp), GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buffer))); +} + +static void +check_buffer_duration (GstBuffer * buffer, GstClockTime duration) +{ + fail_unless (GST_BUFFER_DURATION (buffer) == duration, + "expected duration %" GST_TIME_FORMAT + ", but got duration %" GST_TIME_FORMAT, + GST_TIME_ARGS (duration), GST_TIME_ARGS (GST_BUFFER_DURATION (buffer))); +} + +static void +check_buffer_granulepos (GstBuffer * buffer, GstClockTime granulepos) +{ + fail_unless (GST_BUFFER_OFFSET_END (buffer) == granulepos, + "expected granulepos %" G_GUINT64_FORMAT + ", but got granulepos %" G_GUINT64_FORMAT, + granulepos, GST_BUFFER_OFFSET_END (buffer)); +} + +/* this check is here to check that the granulepos we derive from the timestamp + is about correct. This is "about correct" because you can't precisely go from + timestamp to granulepos due to the downward-rounding characteristics of + gst_util_uint64_scale, so you check if granulepos is equal to the number, or + the number plus one. */ +/* should be from_endtime, but theora's granulepos mapping is "special" */ +static void +check_buffer_granulepos_from_starttime (GstBuffer * buffer, + GstClockTime starttime) +{ + GstClockTime granulepos, expected; + + granulepos = GST_BUFFER_OFFSET_END (buffer); + expected = gst_util_uint64_scale (starttime, FRAMERATE, GST_SECOND); + + fail_unless (granulepos == expected || granulepos == expected + 1, + "expected granulepos %" G_GUINT64_FORMAT + " or %" G_GUINT64_FORMAT + ", but got granulepos %" G_GUINT64_FORMAT, + expected, expected + 1, granulepos); +} + +GST_START_TEST (test_granulepos_offset) +{ + GstElement *bin; + GstPad *pad; + gchar *pipe_str; + GstBuffer *buffer; + GError *error = NULL; + GstClockTime timestamp; + + pipe_str = g_strdup_printf ("videotestsrc timestamp-offset=%" G_GUINT64_FORMAT + " ! video/x-raw-yuv,format=(fourcc)I420,framerate=10/1" + " ! theoraenc ! fakesink", TIMESTAMP_OFFSET); + + bin = gst_parse_launch (pipe_str, &error); + fail_unless (bin != NULL, "Error parsing pipeline: %s", + error ? error->message : "(invalid error)"); + g_free (pipe_str); + + /* get the pad */ + { + GstElement *sink = gst_bin_get_by_name (GST_BIN (bin), "fakesink0"); + + fail_unless (sink != NULL, "Could not get fakesink out of bin"); + pad = gst_element_get_pad (sink, "sink"); + fail_unless (pad != NULL, "Could not get pad out of fakesink"); + gst_object_unref (sink); + } + + start_pipeline (bin, pad); + + /* header packets should have timestamp == NONE, granulepos 0 */ + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE); + check_buffer_duration (buffer, GST_CLOCK_TIME_NONE); + check_buffer_granulepos (buffer, 0); + gst_buffer_unref (buffer); + + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE); + check_buffer_duration (buffer, GST_CLOCK_TIME_NONE); + check_buffer_granulepos (buffer, 0); + gst_buffer_unref (buffer); + + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE); + check_buffer_duration (buffer, GST_CLOCK_TIME_NONE); + check_buffer_granulepos (buffer, 0); + gst_buffer_unref (buffer); + + { + GstClockTime next_timestamp, last_granulepos; + + /* first buffer should have timestamp of TIMESTAMP_OFFSET, granulepos to + * match the timestamp of the end of the last sample in the output buffer. + * Note that one cannot go timestamp->granulepos->timestamp and get the same + * value due to loss of precision with granulepos. theoraenc does take care + * to timestamp correctly based on the offset of the input data however, so + * it does do sub-granulepos timestamping. */ + buffer = get_buffer (bin, pad); + last_granulepos = GST_BUFFER_OFFSET_END (buffer); + check_buffer_timestamp (buffer, TIMESTAMP_OFFSET); + /* don't really have a good way of checking duration... */ + check_buffer_granulepos_from_starttime (buffer, TIMESTAMP_OFFSET); + + next_timestamp = TIMESTAMP_OFFSET + GST_BUFFER_DURATION (buffer); + + gst_buffer_unref (buffer); + + /* check continuity with the next buffer */ + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, next_timestamp); + check_buffer_duration (buffer, + gst_util_uint64_scale (GST_BUFFER_OFFSET_END (buffer), GST_SECOND, + FRAMERATE) + - gst_util_uint64_scale (last_granulepos, GST_SECOND, FRAMERATE)); + check_buffer_granulepos_from_starttime (buffer, next_timestamp); + + gst_buffer_unref (buffer); + } + + stop_pipeline (bin, pad); + + gst_object_unref (pad); + gst_object_unref (bin); +} + +GST_END_TEST; + +GST_START_TEST (test_continuity) +{ + GstElement *bin; + GstPad *pad; + gchar *pipe_str; + GstBuffer *buffer; + GError *error = NULL; + GstClockTime timestamp; + + pipe_str = g_strdup_printf ("videotestsrc" + " ! video/x-raw-yuv,format=(fourcc)I420,framerate=10/1" + " ! theoraenc ! fakesink"); + + bin = gst_parse_launch (pipe_str, &error); + fail_unless (bin != NULL, "Error parsing pipeline: %s", + error ? error->message : "(invalid error)"); + g_free (pipe_str); + + /* get the pad */ + { + GstElement *sink = gst_bin_get_by_name (GST_BIN (bin), "fakesink0"); + + fail_unless (sink != NULL, "Could not get fakesink out of bin"); + pad = gst_element_get_pad (sink, "sink"); + fail_unless (pad != NULL, "Could not get pad out of fakesink"); + gst_object_unref (sink); + } + + start_pipeline (bin, pad); + + /* header packets should have timestamp == NONE, granulepos 0 */ + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE); + check_buffer_duration (buffer, GST_CLOCK_TIME_NONE); + check_buffer_granulepos (buffer, 0); + gst_buffer_unref (buffer); + + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE); + check_buffer_duration (buffer, GST_CLOCK_TIME_NONE); + check_buffer_granulepos (buffer, 0); + gst_buffer_unref (buffer); + + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE); + check_buffer_duration (buffer, GST_CLOCK_TIME_NONE); + check_buffer_granulepos (buffer, 0); + gst_buffer_unref (buffer); + + { + GstClockTime next_timestamp, last_granulepos; + + /* first buffer should have timestamp of TIMESTAMP_OFFSET, granulepos to + * match the timestamp of the end of the last sample in the output buffer. + * Note that one cannot go timestamp->granulepos->timestamp and get the same + * value due to loss of precision with granulepos. theoraenc does take care + * to timestamp correctly based on the offset of the input data however, so + * it does do sub-granulepos timestamping. */ + buffer = get_buffer (bin, pad); + last_granulepos = GST_BUFFER_OFFSET_END (buffer); + check_buffer_timestamp (buffer, 0); + check_buffer_duration (buffer, GST_SECOND / 10); /* plain division because I + know the answer is exact */ + check_buffer_granulepos (buffer, 0); + + next_timestamp = GST_BUFFER_DURATION (buffer); + + gst_buffer_unref (buffer); + + /* check continuity with the next buffer */ + buffer = get_buffer (bin, pad); + check_buffer_timestamp (buffer, next_timestamp); + check_buffer_duration (buffer, GST_SECOND / 10); + check_buffer_granulepos (buffer, 1); + + gst_buffer_unref (buffer); + } + + stop_pipeline (bin, pad); + + gst_object_unref (pad); + gst_object_unref (bin); +} + +GST_END_TEST; + +Suite * +theoraenc_suite (void) +{ + Suite *s = suite_create ("theoraenc"); + TCase *tc_chain = tcase_create ("general"); + + suite_add_tcase (s, tc_chain); + tcase_add_test (tc_chain, test_granulepos_offset); + tcase_add_test (tc_chain, test_continuity); + + return s; +} + +int +main (int argc, char **argv) +{ + int nf; + + Suite *s = theoraenc_suite (); + SRunner *sr = srunner_create (s); + + gst_check_init (&argc, &argv); + + srunner_run_all (sr, CK_NORMAL); + nf = srunner_ntests_failed (sr); + srunner_free (sr); + + return nf; +} diff --git a/tests/check/pipelines/vorbisenc.c b/tests/check/pipelines/vorbisenc.c index 36ae66b..970e7f6 100644 --- a/tests/check/pipelines/vorbisenc.c +++ b/tests/check/pipelines/vorbisenc.c @@ -2,7 +2,7 @@ * * unit test for vorbisenc * - * Copyright (C) <2005> Thomas Vander Stichele + * Copyright (C) 2006 Andy Wingo * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public -- 2.7.4