From 3348c5ceae6ff8df1f64f50f21e65711186678ff Mon Sep 17 00:00:00 2001 From: John-Mark Bell Date: Fri, 8 Sep 2017 20:02:13 +0100 Subject: [PATCH] rtpvp8pay: payload temporally scaled bitstreams. Co-Authored-By: Vincent Sanders Part-of: --- gst/rtp/gstrtpvp8pay.c | 161 +++++++++++++-- gst/rtp/gstrtpvp8pay.h | 2 + tests/check/elements/rtpvp8.c | 469 ++++++++++++++++++++++++++++++++++++++++++ tests/check/meson.build | 1 + 4 files changed, 617 insertions(+), 16 deletions(-) create mode 100644 tests/check/elements/rtpvp8.c diff --git a/gst/rtp/gstrtpvp8pay.c b/gst/rtp/gstrtpvp8pay.c index 880ee77..56edb53 100644 --- a/gst/rtp/gstrtpvp8pay.c +++ b/gst/rtp/gstrtpvp8pay.c @@ -132,11 +132,21 @@ gst_rtp_vp8_pay_picture_id_increment (GstRtpVP8Pay * obj) } static void +gst_rtp_vp8_pay_reset (GstRtpVP8Pay * obj) +{ + gst_rtp_vp8_pay_picture_id_reset (obj); + /* tl0picidx MAY start at a random value, but there's no point. Initialize + * so that first packet will use 0 for convenience */ + obj->tl0picidx = -1; + obj->temporal_scalability_fields_present = FALSE; +} + +static void gst_rtp_vp8_pay_init (GstRtpVP8Pay * obj) { obj->picture_id_mode = DEFAULT_PICTURE_ID_MODE; - gst_rtp_vp8_pay_picture_id_reset (obj); obj->picture_id_offset = DEFAULT_PICTURE_ID_OFFSET; + gst_rtp_vp8_pay_reset (obj); } static void @@ -418,21 +428,38 @@ gst_rtp_vp8_offset_to_partition (GstRtpVP8Pay * self, guint offset) static gsize gst_rtp_vp8_calc_header_len (GstRtpVP8Pay * self) { + gsize len; + switch (self->picture_id_mode) { case VP8_PAY_PICTURE_ID_7BITS: - return 3; + len = 1; + break; case VP8_PAY_PICTURE_ID_15BITS: - return 4; + len = 2; + break; case VP8_PAY_NO_PICTURE_ID: default: - return 1; + len = 0; + break; + } + + if (self->temporal_scalability_fields_present) { + /* Add on space for TL0PICIDX and TID/Y/KEYIDX */ + len += 2; } + + if (len > 0) { + /* All fields above are extension, so allocate space for the ECB field */ + len++; + } + + return len + 1; /* computed + fixed size header */ } /* When growing the vp8 header keep max payload len calculation in sync */ static GstBuffer * gst_rtp_vp8_create_header_buffer (GstRtpVP8Pay * self, guint8 partid, - gboolean start, gboolean mark, GstBuffer * in) + gboolean start, gboolean mark, GstBuffer * in, GstCustomMeta * meta) { GstBuffer *out; guint8 *p; @@ -443,20 +470,73 @@ gst_rtp_vp8_create_header_buffer (GstRtpVP8Pay * self, guint8 partid, (self), gst_rtp_vp8_calc_header_len (self), 0, 0); gst_rtp_buffer_map (out, GST_MAP_READWRITE, &rtpbuffer); p = gst_rtp_buffer_get_payload (&rtpbuffer); + /* X=0,R=0,N=0,S=start,PartID=partid */ p[0] = (start << 4) | partid; - if (self->picture_id_mode != VP8_PAY_NO_PICTURE_ID) { + if (GST_BUFFER_FLAG_IS_SET (in, GST_BUFFER_FLAG_DROPPABLE)) { + /* Enable N=1 */ + p[0] |= 0x20; + } + + if (self->picture_id_mode != VP8_PAY_NO_PICTURE_ID || + self->temporal_scalability_fields_present) { + gint index; + /* Enable X=1 */ p[0] |= 0x80; - /* X: I=1,L=0,T=0,K=0,RSV=0 */ - p[1] = 0x80; + + /* X: I=0,L=0,T=0,K=0,RSV=0 */ + p[1] = 0x00; + if (self->picture_id_mode != VP8_PAY_NO_PICTURE_ID) { + /* Set I bit */ + p[1] |= 0x80; + } + if (self->temporal_scalability_fields_present) { + /* Set L and T bits */ + p[1] |= 0x60; + } + + /* Insert picture ID */ if (self->picture_id_mode == VP8_PAY_PICTURE_ID_7BITS) { /* I: 7 bit picture_id */ p[2] = self->picture_id & 0x7F; - } else { + index = 3; + } else if (self->picture_id_mode == VP8_PAY_PICTURE_ID_15BITS) { /* I: 15 bit picture_id */ p[2] = 0x80 | ((self->picture_id & 0x7FFF) >> 8); p[3] = self->picture_id & 0xFF; + index = 4; + } else { + index = 2; + } + + /* Insert TL0PICIDX and TID/Y/KEYIDX */ + if (self->temporal_scalability_fields_present) { + /* The meta contains tl0picidx from the encoder, but we need to ensure + * that tl0picidx is increasing correctly. The encoder may reset it's + * state and counter, but we cannot. Therefore, we cannot simply copy + * the value into the header.*/ + guint temporal_layer = 0; + gboolean layer_sync = FALSE; + gboolean use_temporal_scaling = FALSE; + + if (meta) { + GstStructure *s = gst_custom_meta_get_structure (meta); + gst_structure_get_boolean (s, "use-temporal-scaling", + &use_temporal_scaling); + + if (use_temporal_scaling) + gst_structure_get (s, "layer-id", G_TYPE_UINT, &temporal_layer, + "layer-sync", G_TYPE_BOOLEAN, &layer_sync, NULL); + } + + /* FIXME: Support a prediction structure where higher layers don't + * necessarily refer to the last base layer frame, ie they use an older + * tl0picidx as signalled in the meta */ + if (temporal_layer == 0 && start) + self->tl0picidx++; + p[index] = self->tl0picidx & 0xFF; + p[index + 1] = ((temporal_layer << 6) | (layer_sync << 5)) & 0xFF; } } @@ -470,15 +550,38 @@ gst_rtp_vp8_create_header_buffer (GstRtpVP8Pay * self, guint8 partid, return out; } +static gboolean +foreach_metadata_drop (GstBuffer * buf, GstMeta ** meta, gpointer user_data) +{ + GstElement *element = user_data; + const GstMetaInfo *info = (*meta)->info; + + if (gst_meta_info_is_custom (info) && + gst_custom_meta_has_name ((GstCustomMeta *) * meta, "GstVP8Meta")) { + GST_DEBUG_OBJECT (element, "dropping GstVP8Meta"); + *meta = NULL; + } + + return TRUE; +} + +static void +gst_rtp_vp8_drop_vp8_meta (gpointer element, GstBuffer * buf) +{ + gst_buffer_foreach_meta (buf, foreach_metadata_drop, element); +} + static guint gst_rtp_vp8_payload_next (GstRtpVP8Pay * self, GstBufferList * list, - guint offset, GstBuffer * buffer, gsize buffer_size, gsize max_payload_len) + guint offset, GstBuffer * buffer, gsize buffer_size, gsize max_payload_len, + GstCustomMeta * meta) { guint partition; GstBuffer *header; GstBuffer *sub; GstBuffer *out; gboolean mark; + gboolean start; gsize remaining; gsize available; @@ -487,16 +590,27 @@ gst_rtp_vp8_payload_next (GstRtpVP8Pay * self, GstBufferList * list, if (available > remaining) available = remaining; - partition = gst_rtp_vp8_offset_to_partition (self, offset); - g_assert (partition < self->n_partitions); + if (meta) { + /* If meta is present, then we have no partition offset information, + * so always emit PID 0 and set the start bit for the first packet + * of a frame only (c.f. RFC7741 $4.4) + */ + partition = 0; + start = (offset == 0); + } else { + partition = gst_rtp_vp8_offset_to_partition (self, offset); + g_assert (partition < self->n_partitions); + start = (offset == self->partition_offset[partition]); + } mark = (remaining == available); /* whole set of partitions, payload them and done */ header = gst_rtp_vp8_create_header_buffer (self, partition, - offset == self->partition_offset[partition], mark, buffer); + start, mark, buffer, meta); sub = gst_buffer_copy_region (buffer, GST_BUFFER_COPY_ALL, offset, available); gst_rtp_copy_video_meta (self, header, buffer); + gst_rtp_vp8_drop_vp8_meta (self, header); out = gst_buffer_append (header, sub); @@ -512,17 +626,31 @@ gst_rtp_vp8_pay_handle_buffer (GstRTPBasePayload * payload, GstBuffer * buffer) GstRtpVP8Pay *self = GST_RTP_VP8_PAY (payload); GstFlowReturn ret; GstBufferList *list; + GstCustomMeta *meta; gsize size, max_paylen; guint offset, mtu, vp8_hdr_len; size = gst_buffer_get_size (buffer); - + meta = gst_buffer_get_custom_meta (buffer, "GstVP8Meta"); if (G_UNLIKELY (!gst_rtp_vp8_pay_parse_frame (self, buffer, size))) { GST_ELEMENT_ERROR (self, STREAM, ENCODE, (NULL), ("Failed to parse VP8 frame")); return GST_FLOW_ERROR; } + if (meta) { + GstStructure *s = gst_custom_meta_get_structure (meta); + gboolean use_temporal_scaling; + /* For interop it's most likely better to keep the temporal scalability + * fields present if the stream previously had them present. Alternating + * whether these fields are present or not may confuse the receiver. */ + + gst_structure_get_boolean (s, "use-temporal-scaling", + &use_temporal_scaling); + if (use_temporal_scaling) + self->temporal_scalability_fields_present = TRUE; + } + mtu = GST_RTP_BASE_PAYLOAD_MTU (payload); vp8_hdr_len = gst_rtp_vp8_calc_header_len (self); max_paylen = gst_rtp_buffer_calc_payload_len (mtu - vp8_hdr_len, 0, @@ -533,7 +661,8 @@ gst_rtp_vp8_pay_handle_buffer (GstRTPBasePayload * payload, GstBuffer * buffer) offset = 0; while (offset < size) { offset += - gst_rtp_vp8_payload_next (self, list, offset, buffer, size, max_paylen); + gst_rtp_vp8_payload_next (self, list, offset, buffer, size, + max_paylen, meta); } ret = gst_rtp_base_payload_push_list (payload, list); @@ -551,7 +680,7 @@ gst_rtp_vp8_pay_sink_event (GstRTPBasePayload * payload, GstEvent * event) GstRtpVP8Pay *self = GST_RTP_VP8_PAY (payload); if (GST_EVENT_TYPE (event) == GST_EVENT_FLUSH_START) { - gst_rtp_vp8_pay_picture_id_reset (self); + gst_rtp_vp8_pay_reset (self); } return GST_RTP_BASE_PAYLOAD_CLASS (gst_rtp_vp8_pay_parent_class)->sink_event diff --git a/gst/rtp/gstrtpvp8pay.h b/gst/rtp/gstrtpvp8pay.h index 2440a63..febec2a 100644 --- a/gst/rtp/gstrtpvp8pay.h +++ b/gst/rtp/gstrtpvp8pay.h @@ -64,6 +64,8 @@ struct _GstRtpVP8Pay PictureIDMode picture_id_mode; gint picture_id_offset; guint16 picture_id; + gboolean temporal_scalability_fields_present; + guint8 tl0picidx; }; GType gst_rtp_vp8_pay_get_type (void); diff --git a/tests/check/elements/rtpvp8.c b/tests/check/elements/rtpvp8.c new file mode 100644 index 0000000..666bed5 --- /dev/null +++ b/tests/check/elements/rtpvp8.c @@ -0,0 +1,469 @@ +/* GStreamer + * + * Copyright (C) 2016 Pexip AS + * @author Stian Selnes + * + * 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 + +#define RTP_VP8_CAPS_STR \ + "application/x-rtp,media=video,encoding-name=VP8,clock-rate=90000,payload=96" + +#define gst_buffer_new_from_array(array) gst_buffer_new_wrapped ( \ + g_memdup (vp8_bitstream_payload, sizeof (vp8_bitstream_payload)), \ + sizeof (vp8_bitstream_payload)) + +static void +add_vp8_meta (GstBuffer * buffer, gboolean use_temporal_scaling, + gboolean layer_sync, guint layer_id, guint tl0picidx) +{ + GstCustomMeta *meta; + GstStructure *s; + + meta = gst_buffer_add_custom_meta (buffer, "GstVP8Meta"); + fail_unless (meta != NULL); + s = gst_custom_meta_get_structure (meta); + gst_structure_set (s, + "use-temporal-scaling", G_TYPE_BOOLEAN, use_temporal_scaling, + "layer-sync", G_TYPE_BOOLEAN, layer_sync, + "layer-id", G_TYPE_UINT, layer_id, + "tl0picidx", G_TYPE_UINT, tl0picidx, NULL); +} + +/* PictureID emum is not exported */ +enum PictureID +{ + VP8_PAY_NO_PICTURE_ID = 0, + VP8_PAY_PICTURE_ID_7BITS = 1, + VP8_PAY_PICTURE_ID_15BITS = 2, +}; + +static const struct no_meta_test_data +{ + /* control inputs */ + enum PictureID pid; /* picture ID type of test */ + gboolean vp8_payload_header_m_flag; + + /* expected outputs */ + guint vp8_payload_header_size; + guint vp8_payload_control_value; +} no_meta_test_data[] = { + { + VP8_PAY_NO_PICTURE_ID, FALSE, 1, 0x10}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, 3, 0x90}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, 4, 0x90}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ + /* repeated with non reference frame */ + { + VP8_PAY_NO_PICTURE_ID, FALSE, 1, 0x30}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, 3, 0xB0}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, 4, 0xB0}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ +}; + +GST_START_TEST (test_pay_no_meta) +{ + guint8 vp8_bitstream_payload[] = { + 0x30, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0xb0, 0x00, 0x90, 0x00, 0x06, 0x47, + 0x08, 0x85, 0x85, 0x88, 0x99, 0x84, 0x88, 0x21, 0x00 + }; + const struct no_meta_test_data *test_data = &no_meta_test_data[__i__]; + GstBuffer *buffer; + GstMapInfo map = GST_MAP_INFO_INIT; + GstHarness *h = gst_harness_new ("rtpvp8pay"); + gst_harness_set_src_caps_str (h, "video/x-vp8"); + + /* check unknown picture id enum value */ + fail_unless (test_data->pid <= VP8_PAY_PICTURE_ID_15BITS); + + g_object_set (h->element, "picture-id-mode", test_data->pid, + "picture-id-offset", 0x5A5A, NULL); + + buffer = gst_buffer_new_wrapped (g_memdup (vp8_bitstream_payload, + sizeof (vp8_bitstream_payload)), sizeof (vp8_bitstream_payload)); + + /* set droppable if N flag set */ + if ((test_data->vp8_payload_control_value & 0x20) != 0) { + GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_DROPPABLE); + } + + buffer = gst_harness_push_and_pull (h, buffer); + + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless (map.data != NULL); + + /* check buffer size and content */ + fail_unless_equals_int (map.size, + 12 + test_data->vp8_payload_header_size + sizeof (vp8_bitstream_payload)); + + fail_unless_equals_int (test_data->vp8_payload_control_value, map.data[12]); + + if (test_data->vp8_payload_header_size > 2) { + /* vp8 header extension byte must have I set */ + fail_unless_equals_int (0x80, map.data[13]); + /* check picture id */ + if (test_data->pid == VP8_PAY_PICTURE_ID_7BITS) { + fail_unless_equals_int (0x5a, map.data[14]); + } else if (test_data->pid == VP8_PAY_PICTURE_ID_15BITS) { + fail_unless_equals_int (0xDA, map.data[14]); + fail_unless_equals_int (0x5A, map.data[15]); + } + } + + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +static const struct with_meta_test_data +{ + /* control inputs */ + enum PictureID pid; /* picture ID type of test */ + gboolean vp8_payload_header_m_flag; + gboolean use_temporal_scaling; + gboolean y_flag; + + /* expected outputs */ + guint vp8_payload_header_size; + guint vp8_payload_control_value; + guint vp8_payload_extended_value; +} with_meta_test_data[] = { + { + VP8_PAY_NO_PICTURE_ID, FALSE, FALSE, FALSE, 1, 0x10, 0x80}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, FALSE, FALSE, 3, 0x90, 0x80}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, FALSE, FALSE, 4, 0x90, 0x80}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ + { + VP8_PAY_NO_PICTURE_ID, FALSE, TRUE, FALSE, 4, 0x90, 0x60}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, TRUE, FALSE, 5, 0x90, 0xE0}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, TRUE, FALSE, 6, 0x90, 0xE0}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ + { + VP8_PAY_NO_PICTURE_ID, FALSE, TRUE, TRUE, 4, 0x90, 0x60}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, TRUE, TRUE, 5, 0x90, 0xE0}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, TRUE, TRUE, 6, 0x90, 0xE0}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ + /* repeated with non reference frame */ + { + VP8_PAY_NO_PICTURE_ID, FALSE, FALSE, FALSE, 1, 0x30, 0x80}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, FALSE, FALSE, 3, 0xB0, 0x80}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, FALSE, FALSE, 4, 0xB0, 0x80}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ + { + VP8_PAY_NO_PICTURE_ID, FALSE, TRUE, FALSE, 4, 0xB0, 0x60}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, TRUE, FALSE, 5, 0xB0, 0xE0}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, TRUE, FALSE, 6, 0xB0, 0xE0}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ + { + VP8_PAY_NO_PICTURE_ID, FALSE, TRUE, TRUE, 4, 0xB0, 0x60}, /* no picture ID single byte header, S set */ + { + VP8_PAY_PICTURE_ID_7BITS, FALSE, TRUE, TRUE, 5, 0xB0, 0xE0}, /* X bit to allow for I bit means header is three bytes, S and X set */ + { + VP8_PAY_PICTURE_ID_15BITS, TRUE, TRUE, TRUE, 6, 0xB0, 0xE0}, /* X bit to allow for I bit with M bit means header is four bytes, S, X and M set */ +}; + +GST_START_TEST (test_pay_with_meta) +{ + guint8 vp8_bitstream_payload[] = { + 0x30, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0xb0, 0x00, 0x90, 0x00, 0x06, 0x47, + 0x08, 0x85, 0x85, 0x88, 0x99, 0x84, 0x88, 0x21, 0x00 + }; + const struct with_meta_test_data *test_data = &with_meta_test_data[__i__]; + GstBuffer *buffer; + GstCustomMeta *meta; + GstMapInfo map = GST_MAP_INFO_INIT; + GstHarness *h = gst_harness_new ("rtpvp8pay"); + gst_harness_set_src_caps_str (h, "video/x-vp8"); + + /* check for unknown picture id enum value */ + fail_unless (test_data->pid <= VP8_PAY_PICTURE_ID_15BITS); + + g_object_set (h->element, "picture-id-mode", test_data->pid, + "picture-id-offset", 0x5A5A, NULL); + + /* Push a buffer in */ + buffer = gst_buffer_new_wrapped (g_memdup (vp8_bitstream_payload, + sizeof (vp8_bitstream_payload)), sizeof (vp8_bitstream_payload)); + add_vp8_meta (buffer, test_data->use_temporal_scaling, test_data->y_flag, + 2, 255); + /* set droppable if N flag set */ + if ((test_data->vp8_payload_control_value & 0x20) != 0) { + GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_DROPPABLE); + } + + buffer = gst_harness_push_and_pull (h, buffer); + + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless (map.data != NULL); + + meta = gst_buffer_get_custom_meta (buffer, "GstVP8Meta"); + fail_unless (meta == NULL); + + /* check buffer size and content */ + fail_unless_equals_int (map.size, + 12 + test_data->vp8_payload_header_size + sizeof (vp8_bitstream_payload)); + fail_unless_equals_int (test_data->vp8_payload_control_value, map.data[12]); + + if (test_data->vp8_payload_header_size > 1) { + int hdridx = 13; + fail_unless_equals_int (test_data->vp8_payload_extended_value, + map.data[hdridx++]); + + /* check picture ID */ + if (test_data->pid == VP8_PAY_PICTURE_ID_7BITS) { + fail_unless_equals_int (0x5A, map.data[hdridx++]); + } else if (test_data->pid == VP8_PAY_PICTURE_ID_15BITS) { + fail_unless_equals_int (0xDA, map.data[hdridx++]); + fail_unless_equals_int (0x5A, map.data[hdridx++]); + } + + if (test_data->use_temporal_scaling) { + /* check temporal layer 0 picture ID value */ + fail_unless_equals_int (255, map.data[hdridx++]); + /* check temporal layer ID value */ + fail_unless_equals_int (2, (map.data[hdridx] >> 6) & 0x3); + + if (test_data->y_flag) { + fail_unless_equals_int (1, (map.data[hdridx] >> 5) & 1); + } else { + fail_unless_equals_int (0, (map.data[hdridx] >> 5) & 1); + } + } + } + + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_pay_continuous_picture_id_and_tl0picidx) +{ + guint8 vp8_bitstream_payload[] = { + 0x30, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0xb0, 0x00, 0x90, 0x00, 0x06, 0x47, + 0x08, 0x85, 0x85, 0x88, 0x99, 0x84, 0x88, 0x21, 0x00 + }; + GstHarness *h = gst_harness_new ("rtpvp8pay"); + const gint header_len_without_tl0picidx = 3; + const gint header_len_with_tl0picidx = 5; + const gint packet_len_without_tl0picidx = 12 + header_len_without_tl0picidx + + sizeof (vp8_bitstream_payload); + const gint packet_len_with_tl0picidx = 12 + header_len_with_tl0picidx + + sizeof (vp8_bitstream_payload); + const gint picid_offset = 14; + const gint tl0picidx_offset = 15; + GstBuffer *buffer; + GstMapInfo map; + + g_object_set (h->element, "picture-id-mode", VP8_PAY_PICTURE_ID_7BITS, + "picture-id-offset", 0, NULL); + gst_harness_set_src_caps_str (h, "video/x-vp8"); + + /* First, push a frame without temporal scalability meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + buffer = gst_harness_push_and_pull (h, buffer); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len_without_tl0picidx); + fail_unless_equals_int (map.data[picid_offset], 0x00); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + /* Push a frame for temporal layer 0 with meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 0, 0); + + buffer = gst_harness_push_and_pull (h, buffer); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len_with_tl0picidx); + fail_unless_equals_int (map.data[picid_offset], 0x01); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x00); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + /* Push a frame for temporal layer 1 with meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 1, 0); + buffer = gst_harness_push_and_pull (h, buffer); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len_with_tl0picidx); + fail_unless_equals_int (map.data[picid_offset], 0x02); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x00); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + /* Push next frame for temporal layer 0 with meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 0, 1); + buffer = gst_harness_push_and_pull (h, buffer); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len_with_tl0picidx); + fail_unless_equals_int (map.data[picid_offset], 0x03); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x01); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + /* Another frame for temporal layer 0, but now the meta->tl0picidx has been + * reset to 0 (simulating an encoder reset). Payload must ensure tl0picidx + * is increasing. */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 0, 0); + buffer = gst_harness_push_and_pull (h, buffer); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len_with_tl0picidx); + fail_unless_equals_int (map.data[picid_offset], 0x04); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x02); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + /* If we receive a frame without meta, we should continue to increase and + * add tl0picidx (assuming TID=0) in order to maximize interop. */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + buffer = gst_harness_push_and_pull (h, buffer); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len_with_tl0picidx); + fail_unless_equals_int (map.data[picid_offset], 0x05); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x03); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + + gst_harness_teardown (h); +} + +GST_END_TEST; + +GST_START_TEST (test_pay_tl0picidx_split_buffer) +{ + guint8 vp8_bitstream_payload[] = { + 0x30, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0xb0, 0x00, 0x90, 0x00, 0x06, 0x47, + 0x08, 0x85, 0x85, 0x88, 0x99, 0x84, 0x88, 0x21, 0x00 + }; + GstHarness *h = + gst_harness_new_parse + ("rtpvp8pay mtu=28 picture-id-mode=1 picture-id-offset=0"); + const gint header_len = 12 + 5; /* RTP + VP8 payload header */ + const gint picid_offset = 14; + const gint tl0picidx_offset = 15; + guint output_bytes_left; + GstBuffer *buffer; + GstMapInfo map; + + gst_harness_set_src_caps_str (h, "video/x-vp8"); + + /* Push a frame for temporal layer 0 with meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 0, 0); + gst_harness_push (h, buffer); + + /* Expect it to be split into multiple buffers to fit the MTU */ + output_bytes_left = sizeof (vp8_bitstream_payload); + while (output_bytes_left > 0) { + const gint expected = MIN (output_bytes_left, 28 - header_len); + const gint packet_len = header_len + expected; + output_bytes_left -= expected; + + buffer = gst_harness_pull (h); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len); + fail_unless_equals_int (map.data[picid_offset], 0x00); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x00); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + } + + /* Push a frame for temporal layer 1 with meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 1, 0); + gst_harness_push (h, buffer); + + /* Expect it to be split into multiple buffers to fit the MTU */ + output_bytes_left = sizeof (vp8_bitstream_payload); + while (output_bytes_left > 0) { + const gint expected = MIN (output_bytes_left, 28 - header_len); + const gint packet_len = header_len + expected; + output_bytes_left -= expected; + + buffer = gst_harness_pull (h); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len); + fail_unless_equals_int (map.data[picid_offset], 0x01); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x00); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + } + + /* Push another frame for temporal layer 0 with meta */ + buffer = gst_buffer_new_from_array (vp8_bitstream_payload); + add_vp8_meta (buffer, TRUE, TRUE, 0, 0); + gst_harness_push (h, buffer); + + /* Expect it to be split into multiple buffers to fit the MTU */ + output_bytes_left = sizeof (vp8_bitstream_payload); + while (output_bytes_left > 0) { + const gint expected = MIN (output_bytes_left, 28 - header_len); + const gint packet_len = header_len + expected; + output_bytes_left -= expected; + + buffer = gst_harness_pull (h); + fail_unless (gst_buffer_map (buffer, &map, GST_MAP_READ)); + fail_unless_equals_int (map.size, packet_len); + fail_unless_equals_int (map.data[picid_offset], 0x02); + fail_unless_equals_int (map.data[tl0picidx_offset], 0x01); + gst_buffer_unmap (buffer, &map); + gst_buffer_unref (buffer); + } + + gst_harness_teardown (h); +} + +GST_END_TEST; + +static Suite * +rtpvp8_suite (void) +{ + Suite *s = suite_create ("rtpvp8"); + TCase *tc_chain; + static const gchar *tags[] = { NULL }; + + /* Register custom GstVP8Meta manually */ + gst_meta_register_custom ("GstVP8Meta", tags, NULL, NULL, NULL); + + suite_add_tcase (s, (tc_chain = tcase_create ("vp8pay"))); + tcase_add_loop_test (tc_chain, test_pay_no_meta, 0, + G_N_ELEMENTS (no_meta_test_data)); + tcase_add_loop_test (tc_chain, test_pay_with_meta, 0, + G_N_ELEMENTS (with_meta_test_data)); + tcase_add_test (tc_chain, test_pay_continuous_picture_id_and_tl0picidx); + tcase_add_test (tc_chain, test_pay_tl0picidx_split_buffer); + + return s; +} + +GST_CHECK_MAIN (rtpvp8); diff --git a/tests/check/meson.build b/tests/check/meson.build index 2244a03..57bc18a 100644 --- a/tests/check/meson.build +++ b/tests/check/meson.build @@ -66,6 +66,7 @@ good_tests = [ [ 'elements/rtph264' ], [ 'elements/rtph265' ], [ 'elements/rtpopus' ], + [ 'elements/rtpvp8' ], [ 'elements/rtpvp9' ], [ 'elements/rtpbin' ], [ 'elements/rtpbin_buffer_list' ], -- 2.7.4