basetsmux: rework SCTE section handling to handle passthrough

mpegtsmux can receive SCTE sections from two origins: events
created by the application, and events forwarded downstream by
mpegtsdemux, containing sections that may not have been fully
parsed, and additional data to help tsmux translate times to
the correct domain, both for requesting keyframes and calculating
an accurate pts_adjustment.

The complete approach is documented further in a comment above
the relevant function.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/913>
This commit is contained in:
Mathieu Duponchelle 2021-04-23 01:22:32 +02:00 committed by GStreamer Marge Bot
parent e4f40ba526
commit c3a161f287
3 changed files with 309 additions and 104 deletions

View file

@ -467,6 +467,8 @@ gst_mpegts_scte_sit_new (void)
sit->descriptors = g_ptr_array_new_with_free_func ((GDestroyNotify)
gst_mpegts_descriptor_free);
sit->is_running_time = TRUE;
return sit;
}
@ -483,6 +485,9 @@ gst_mpegts_scte_null_new (void)
GstMpegtsSCTESIT *sit = gst_mpegts_scte_sit_new ();
sit->splice_command_type = GST_MTS_SCTE_SPLICE_COMMAND_NULL;
sit->is_running_time = TRUE;
return sit;
}
@ -506,6 +511,8 @@ gst_mpegts_scte_cancel_new (guint32 event_id)
event->splice_event_cancel_indicator = TRUE;
g_ptr_array_add (sit->splices, event);
sit->is_running_time = TRUE;
return sit;
}
@ -539,6 +546,8 @@ gst_mpegts_scte_splice_in_new (guint32 event_id, GstClockTime splice_time)
}
g_ptr_array_add (sit->splices, event);
sit->is_running_time = TRUE;
return sit;
}
@ -582,6 +591,8 @@ gst_mpegts_scte_splice_out_new (guint32 event_id, GstClockTime splice_time,
}
g_ptr_array_add (sit->splices, event);
sit->is_running_time = TRUE;
return sit;
}

View file

@ -185,10 +185,6 @@ struct _GstMpegtsSCTESIT
guint16 splice_command_length;
/* When encrypted, or when encountering an unknown command type,
* we may still want to pass the sit through */
gboolean fully_parsed;
GstMpegtsSCTESpliceCommandType splice_command_type;
/* For time_signal commands */
@ -198,6 +194,13 @@ struct _GstMpegtsSCTESIT
GPtrArray *splices;
GPtrArray *descriptors;
/* When encrypted, or when encountering an unknown command type,
* we may still want to pass the sit through */
gboolean fully_parsed;
/* When the SIT was constructed by the application, splice times
* are in running_time and must be translated before packetizing */
gboolean is_running_time;
};
GST_MPEGTS_API

View file

@ -1420,6 +1420,82 @@ gst_base_ts_mux_release_pad (GstElement * element, GstPad * pad)
GST_ELEMENT_CLASS (parent_class)->release_pad (element, pad);
}
/* GstAggregator implementation */
static void
request_keyframe (GstBaseTsMux * mux, GstClockTime running_time)
{
GList *l;
GST_OBJECT_LOCK (mux);
for (l = GST_ELEMENT_CAST (mux)->sinkpads; l; l = l->next) {
gst_pad_push_event (GST_PAD (l->data),
gst_video_event_new_upstream_force_key_unit (running_time, TRUE, 0));
}
GST_OBJECT_UNLOCK (mux);
}
static const guint32 crc_tab[256] = {
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b,
0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7,
0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3,
0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef,
0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb,
0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0,
0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4,
0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08,
0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc,
0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050,
0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34,
0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1,
0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5,
0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9,
0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd,
0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71,
0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2,
0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e,
0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a,
0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676,
0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662,
0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
};
static guint32
_calc_crc32 (const guint8 * data, guint datalen)
{
gint i;
guint32 crc = 0xffffffff;
for (i = 0; i < datalen; i++) {
crc = (crc << 8) ^ crc_tab[((crc >> 24) ^ *data++) & 0xff];
}
return crc;
}
#define MPEGTIME_TO_GSTTIME(t) ((t) * (guint64)100000 / 9)
static GstMpegtsSCTESpliceEvent *
copy_splice (GstMpegtsSCTESpliceEvent * splice)
{
@ -1439,94 +1515,149 @@ deep_copy_sit (const GstMpegtsSCTESIT * sit)
return sit_copy;
}
/* GstAggregator implementation */
/* Takes ownership of @section.
*
* This function is a bit complex because the SCTE sections can
* have various origins:
*
* * Sections created by the application with the gst_mpegts_scte_*_new()
* API. The splice times / durations contained by these are expressed
* in the GStreamer running time domain, and must be translated to
* our local PES time domain. In this case, we will packetize the section
* ourselves.
*
* * Sections passed through from tsdemux: this case is complicated as
* splice times in the incoming stream may be encrypted, with pts_adjustment
* being the only timing field guaranteed *not* to be encrypted. In this
* case, the original binary data (section->data) will be reinjected as is
* in the output stream, with pts_adjustment adjusted. tsdemux provides us
* with the pts_offset it introduces, the difference between the original
* PES PTSs and the running times it outputs.
*
* Additionally, in either of these cases when the splice times aren't encrypted
* we want to make use of those to request keyframes. For the passthrough case,
* as the splice times are left untouched tsdemux provides us with the running
* times the section originally referred to. We cannot calculate it locally
* because we would need to have access to the information that the timestamps
* in the original PES domain have wrapped around, and how many times they have
* done so. While we could probably make educated guesses, tsdemux (more specifically
* mpegtspacketizer) already keeps track of that, and it seemed more logical to
* perform the calculation there and forward it alongside the downstream events.
*
* Finally, while we can't request keyframes at splice points in the encrypted
* case, if the input stream was compliant in that regard and no reencoding took
* place the splice times will still match with valid splice points, it is up
* to the application to ensure that that is the case.
*/
static void
request_keyframe (GstBaseTsMux * mux, GstClockTime running_time)
handle_scte35_section (GstBaseTsMux * mux, GstMpegtsSection * section,
guint64 mpeg_pts_offset, GstStructure * rtime_map)
{
GList *l;
GST_OBJECT_LOCK (mux);
for (l = GST_ELEMENT_CAST (mux)->sinkpads; l; l = l->next) {
gst_pad_push_event (GST_PAD (l->data),
gst_video_event_new_upstream_force_key_unit (running_time, TRUE, 0));
}
GST_OBJECT_UNLOCK (mux);
}
/* Takes ownership of @section */
static void
handle_scte35_section (GstBaseTsMux * mux, GstMpegtsSection * section)
{
const GstMpegtsSCTESIT *sit;
GstMpegtsSCTESIT *sit_copy;
GstMpegtsSCTESIT *sit;
guint i;
gboolean forward = TRUE;
guint64 pts_adjust;
guint8 *section_data;
guint8 *crc;
gboolean translate = FALSE;
sit = gst_mpegts_section_get_scte_sit (section);
sit_copy = deep_copy_sit (sit);
sit = (GstMpegtsSCTESIT *) gst_mpegts_section_get_scte_sit (section);
switch (sit_copy->splice_command_type) {
/* When the application injects manually constructed splice events,
* their time domain is the GStreamer running time, we receive them
* unpacketized and translate the fields in the SIT to local PTS.
*
* We make a copy of the SIT in order to make sure we can rewrite it.
*/
if (sit->is_running_time) {
sit = deep_copy_sit (sit);
translate = TRUE;
}
switch (sit->splice_command_type) {
case GST_MTS_SCTE_SPLICE_COMMAND_NULL:
/* We implement heartbeating ourselves */
forward = FALSE;
break;
case GST_MTS_SCTE_SPLICE_COMMAND_SCHEDULE:
/* Only translate timestamps and forward, splice_insert
/* No need to request keyframes at this point, splice_insert
* messages will precede the future splice points and we
* can request keyframes then.
* can request keyframes then. Only translate if needed.
*/
for (i = 0; i < sit_copy->splices->len; i++) {
GstMpegtsSCTESpliceEvent *sevent =
g_ptr_array_index (sit_copy->splices, i);
if (sevent->program_splice_time_specified) {
sevent->program_splice_time =
GSTTIME_TO_MPEGTIME (sevent->program_splice_time) +
TS_MUX_CLOCK_BASE;
}
if (sevent->duration_flag) {
sevent->break_duration = GSTTIME_TO_MPEGTIME (sevent->break_duration);
if (translate) {
for (i = 0; i < sit->splices->len; i++) {
GstMpegtsSCTESpliceEvent *sevent =
g_ptr_array_index (sit->splices, i);
if (sevent->program_splice_time_specified)
sevent->program_splice_time =
GSTTIME_TO_MPEGTIME (sevent->program_splice_time) +
TS_MUX_CLOCK_BASE;
if (sevent->duration_flag)
sevent->break_duration =
GSTTIME_TO_MPEGTIME (sevent->break_duration);
}
}
break;
case GST_MTS_SCTE_SPLICE_COMMAND_INSERT:
/* We want keyframes at splice points */
for (i = 0; i < sit_copy->splices->len; i++) {
guint64 running_time = GST_CLOCK_TIME_NONE;
if (sit->fully_parsed && (rtime_map || translate)) {
GstMpegtsSCTESpliceEvent *sevent =
g_ptr_array_index (sit_copy->splices, i);
if (sevent->program_splice_time_specified) {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for splice point at %" GST_TIME_FORMAT,
GST_TIME_ARGS (sevent->program_splice_time));
running_time = sevent->program_splice_time;
request_keyframe (mux, running_time);
sevent->program_splice_time =
GSTTIME_TO_MPEGTIME (sevent->program_splice_time) +
TS_MUX_CLOCK_BASE;
} else {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for immediate splice point");
request_keyframe (mux, GST_CLOCK_TIME_NONE);
}
for (i = 0; i < sit->splices->len; i++) {
guint64 running_time = GST_CLOCK_TIME_NONE;
if (sevent->duration_flag) {
/* Even if auto_return is FALSE, when a break_duration is specified it
* is intended as a redundancy mechanism in case the follow-up
* splice insert goes missing.
*
* Schedule a keyframe at that point (if we can calculate its position
* accurately).
*/
if (GST_CLOCK_STIME_IS_VALID (running_time)) {
GstMpegtsSCTESpliceEvent *sevent =
g_ptr_array_index (sit->splices, i);
if (sevent->program_splice_time_specified) {
if (rtime_map) {
gchar *field_name = g_strdup_printf ("event-%u-splice-time",
sevent->splice_event_id);
if (gst_structure_get_uint64 (rtime_map, field_name,
&running_time)) {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for splice point at %" GST_TIME_FORMAT,
GST_TIME_ARGS (running_time));
request_keyframe (mux, running_time);
}
g_free (field_name);
} else {
g_assert (translate == TRUE);
running_time = sevent->program_splice_time;
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for splice point at %" GST_TIME_FORMAT,
GST_TIME_ARGS (running_time));
request_keyframe (mux, running_time);
sevent->program_splice_time =
GSTTIME_TO_MPEGTIME (running_time) + TS_MUX_CLOCK_BASE;
}
} else {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for end of break at %" GST_TIME_FORMAT,
GST_TIME_ARGS (running_time + sevent->break_duration));
request_keyframe (mux, running_time + sevent->break_duration);
"Requesting keyframe for immediate splice point");
request_keyframe (mux, GST_CLOCK_TIME_NONE);
}
if (sevent->duration_flag) {
if (translate) {
sevent->break_duration =
GSTTIME_TO_MPEGTIME (sevent->break_duration);
}
/* Even if auto_return is FALSE, when a break_duration is specified it
* is intended as a redundancy mechanism in case the follow-up
* splice insert goes missing.
*
* Schedule a keyframe at that point (if we can calculate its position
* accurately).
*/
if (GST_CLOCK_TIME_IS_VALID (running_time)) {
running_time += MPEGTIME_TO_GSTTIME (sevent->break_duration);
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for end of break at %" GST_TIME_FORMAT,
GST_TIME_ARGS (running_time));
request_keyframe (mux, running_time);
}
}
sevent->break_duration = GSTTIME_TO_MPEGTIME (sevent->break_duration);
}
}
break;
@ -1539,37 +1670,55 @@ handle_scte35_section (GstBaseTsMux * mux, GstMpegtsSection * section)
* of the requirement in 10.3.4 that a keyframe should not be created
* when the signal contains only a time_descriptor.
*/
for (i = 0; i < sit_copy->descriptors->len; i++) {
GstMpegtsDescriptor *descriptor =
g_ptr_array_index (sit_copy->descriptors, i);
if (sit->fully_parsed && (rtime_map || translate)) {
for (i = 0; i < sit->descriptors->len; i++) {
GstMpegtsDescriptor *descriptor =
g_ptr_array_index (sit->descriptors, i);
switch (descriptor->tag) {
case GST_MTS_SCTE_DESC_AVAIL:
case GST_MTS_SCTE_DESC_DTMF:
case GST_MTS_SCTE_DESC_SEGMENTATION:
do_request_keyframes = TRUE;
break;
case GST_MTS_SCTE_DESC_TIME:
case GST_MTS_SCTE_DESC_AUDIO:
switch (descriptor->tag) {
case GST_MTS_SCTE_DESC_AVAIL:
case GST_MTS_SCTE_DESC_DTMF:
case GST_MTS_SCTE_DESC_SEGMENTATION:
do_request_keyframes = TRUE;
break;
case GST_MTS_SCTE_DESC_TIME:
case GST_MTS_SCTE_DESC_AUDIO:
break;
}
if (do_request_keyframes)
break;
}
if (do_request_keyframes)
break;
}
if (sit->splice_time_specified) {
GstClockTime running_time = GST_CLOCK_TIME_NONE;
if (sit_copy->splice_time_specified) {
if (do_request_keyframes) {
if (rtime_map) {
if (do_request_keyframes
&& gst_structure_get_uint64 (rtime_map, "splice-time",
&running_time)) {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for time signal at %" GST_TIME_FORMAT,
GST_TIME_ARGS (running_time));
request_keyframe (mux, running_time);
}
} else {
g_assert (translate);
running_time = sit->splice_time;
sit->splice_time =
GSTTIME_TO_MPEGTIME (running_time) + TS_MUX_CLOCK_BASE;
if (do_request_keyframes) {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for time signal at %" GST_TIME_FORMAT,
GST_TIME_ARGS (running_time));
request_keyframe (mux, running_time);
}
}
} else if (do_request_keyframes) {
GST_DEBUG_OBJECT (mux,
"Requesting keyframe for time signal at %" GST_TIME_FORMAT,
GST_TIME_ARGS (sit_copy->splice_time));
request_keyframe (mux, sit_copy->splice_time);
"Requesting keyframe for immediate time signal");
request_keyframe (mux, GST_CLOCK_TIME_NONE);
}
sit_copy->splice_time =
GSTTIME_TO_MPEGTIME (sit_copy->splice_time) + TS_MUX_CLOCK_BASE;
} else if (do_request_keyframes) {
GST_DEBUG_OBJECT (mux, "Requesting keyframe for immediate time signal");
request_keyframe (mux, GST_CLOCK_TIME_NONE);
}
break;
}
@ -1586,13 +1735,47 @@ handle_scte35_section (GstBaseTsMux * mux, GstMpegtsSection * section)
return;
}
GST_OBJECT_LOCK (mux);
GST_DEBUG_OBJECT (mux, "Storing SCTE section");
if (mux->pending_scte35_section)
gst_mpegts_section_unref (mux->pending_scte35_section);
mux->pending_scte35_section =
gst_mpegts_section_from_scte_sit (sit_copy, mux->scte35_pid);
GST_OBJECT_UNLOCK (mux);
if (!translate) {
g_assert (section->data);
/* Calculate the final adjustment, as a sum of:
* - The adjustment in the original packet
* - The offset introduced between the original local PTS
* and the GStreamer PTS output by tsdemux
* - Our own 1-hour offset
*/
pts_adjust = sit->pts_adjustment + mpeg_pts_offset + TS_MUX_CLOCK_BASE;
pts_adjust &= 0x1ffffffff;
section_data = g_memdup (section->data, section->section_length);
section_data[4] |= pts_adjust >> 32;
section_data[5] = pts_adjust >> 24;
section_data[6] = pts_adjust >> 16;
section_data[7] = pts_adjust >> 8;
section_data[8] = pts_adjust;
/* Now rewrite our checksum */
crc = section_data + section->section_length - 4;
GST_WRITE_UINT32_BE (crc, _calc_crc32 (section_data, crc - section_data));
GST_OBJECT_LOCK (mux);
GST_DEBUG_OBJECT (mux, "Storing SCTE section");
if (mux->pending_scte35_section)
gst_mpegts_section_unref (mux->pending_scte35_section);
mux->pending_scte35_section =
gst_mpegts_section_new (mux->scte35_pid, section_data,
section->section_length);
GST_OBJECT_UNLOCK (mux);
gst_mpegts_section_unref (section);
} else {
GST_OBJECT_LOCK (mux);
GST_DEBUG_OBJECT (mux, "Storing SCTE section");
gst_mpegts_section_unref (section);
if (mux->pending_scte35_section)
gst_mpegts_section_unref (mux->pending_scte35_section);
mux->pending_scte35_section =
gst_mpegts_section_from_scte_sit (sit, mux->scte35_pid);;
GST_OBJECT_UNLOCK (mux);
}
}
static gboolean
@ -1607,7 +1790,7 @@ gst_base_ts_mux_send_event (GstElement * element, GstEvent * event)
GST_DEBUG ("Received event with mpegts section");
if (section->section_type == GST_MPEGTS_SECTION_SCTE_SIT) {
handle_scte35_section (mux, section);
handle_scte35_section (mux, section, 0, NULL);
} else {
/* TODO: Check that the section type is supported */
tsmux_add_mpegts_si_section (mux->tsmux, section);
@ -1656,9 +1839,17 @@ gst_base_ts_mux_sink_event (GstAggregator * agg, GstAggregatorPad * agg_pad,
gst_structure_get (s, "section", GST_TYPE_MPEGTS_SECTION, &section,
NULL);
if (section) {
handle_scte35_section (mux, section);
guint64 mpeg_pts_offset = 0;
GstStructure *rtime_map = NULL;
gst_structure_get (s, "running-time-map", GST_TYPE_STRUCTURE,
&rtime_map, NULL);
gst_structure_get_uint64 (s, "mpeg-pts-offset", &mpeg_pts_offset);
handle_scte35_section (mux, section, mpeg_pts_offset, rtime_map);
if (rtime_map)
gst_structure_free (rtime_map);
mux->last_scte35_event_seqnum = gst_event_get_seqnum (event);
} else {
GST_WARNING_OBJECT (ts_pad,