/*
* gst-scte-section.h -
* Copyright (C) 2013, CableLabs, Louisville, CO 80027
+ * (c) 2019, Centricular ltd
*
* Authors:
* RUIH Team <ruih@cablelabs.com>
+ * Edward Hervey <edward@centricular.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
PROP_SI_INTERVAL,
PROP_BITRATE,
PROP_PCR_INTERVAL,
+ PROP_SCTE_35_PID,
+ PROP_SCTE_35_NULL_INTERVAL
};
+#define DEFAULT_SCTE_35_PID 0
+
#define BASETSMUX_DEFAULT_ALIGNMENT -1
#define CLOCK_BASE 9LL
if (ts_pad->prog == NULL)
goto no_program;
tsmux_set_pmt_interval (ts_pad->prog, mux->pmt_interval);
- g_hash_table_insert (mux->programs,
- GINT_TO_POINTER (ts_pad->prog_id), ts_pad->prog);
+ tsmux_program_set_scte35_pid (ts_pad->prog, mux->scte35_pid);
+ tsmux_program_set_scte35_interval (ts_pad->prog,
+ mux->scte35_null_interval);
+ g_hash_table_insert (mux->programs, GINT_TO_POINTER (ts_pad->prog_id),
+ ts_pad->prog);
}
if (ts_pad->stream == NULL) {
gint64 dts = GST_CLOCK_STIME_NONE;
gboolean delta = TRUE, header = FALSE;
StreamData *stream_data;
+ GstMpegtsSection *scte_section = NULL;
GST_DEBUG_OBJECT (mux, "Pads collected");
GST_DEBUG_OBJECT (best, "Chose stream for output (PID: 0x%04x)", best->pid);
+ GST_OBJECT_LOCK (mux);
+ scte_section = mux->pending_scte35_section;
+ mux->pending_scte35_section = NULL;
+ GST_OBJECT_UNLOCK (mux);
+ if (G_UNLIKELY (scte_section)) {
+ GST_DEBUG_OBJECT (mux, "Sending pending SCTE section");
+ if (!tsmux_send_section (mux->tsmux, scte_section))
+ GST_ERROR_OBJECT (mux, "Error sending SCTE section !");
+ }
+
if (GST_CLOCK_TIME_IS_VALID (GST_BUFFER_PTS (buf))) {
pts = GSTTIME_TO_MPEGTIME (GST_BUFFER_PTS (buf));
GST_DEBUG_OBJECT (mux, "Buffer has PTS %" GST_TIME_FORMAT " pts %"
if (section) {
GST_DEBUG ("Received event with mpegts section");
- /* TODO: Check that the section type is supported */
- tsmux_add_mpegts_si_section (mux->tsmux, section);
+ if (section->section_type == GST_MPEGTS_SECTION_SCTE_SIT) {
+ /* Will be sent from the streaming threads */
+ GST_DEBUG_OBJECT (mux, "Storing SCTE event");
+ GST_OBJECT_LOCK (element);
+ if (mux->pending_scte35_section)
+ gst_mpegts_section_unref (mux->pending_scte35_section);
+ mux->pending_scte35_section = section;
+ GST_OBJECT_UNLOCK (element);
+ } else {
+ /* TODO: Check that the section type is supported */
+ tsmux_add_mpegts_si_section (mux->tsmux, section);
+ }
gst_event_unref (event);
if (mux->tsmux)
tsmux_set_pcr_interval (mux->tsmux, mux->pcr_interval);
break;
+ case PROP_SCTE_35_PID:
+ mux->scte35_pid = g_value_get_uint (value);
+ break;
+ case PROP_SCTE_35_NULL_INTERVAL:
+ mux->scte35_null_interval = g_value_get_uint (value);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
case PROP_PCR_INTERVAL:
g_value_set_uint (value, mux->pcr_interval);
break;
+ case PROP_SCTE_35_PID:
+ g_value_set_uint (value, mux->scte35_pid);
+ break;
+ case PROP_SCTE_35_NULL_INTERVAL:
+ g_value_set_uint (value, mux->scte35_null_interval);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
1, G_MAXUINT, TSMUX_DEFAULT_PCR_INTERVAL,
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
+ g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_SCTE_35_PID,
+ g_param_spec_uint ("scte-35-pid", "SCTE-35 PID",
+ "PID to use for inserting SCTE-35 packets (0: unused)",
+ 0, G_MAXUINT, DEFAULT_SCTE_35_PID,
+ (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
+
+ g_object_class_install_property (G_OBJECT_CLASS (klass),
+ PROP_SCTE_35_NULL_INTERVAL, g_param_spec_uint ("scte-35-null-interval",
+ "SCTE-35 NULL packet interval",
+ "Set the interval (in ticks of the 90kHz clock) for writing SCTE-35 NULL (heartbeat) packets."
+ " (only valid if scte-35-pid is different from 0)", 1, G_MAXUINT,
+ TSMUX_DEFAULT_SCTE_35_NULL_INTERVAL,
+ (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
+
gst_element_class_add_static_pad_template_with_gtype (gstelement_class,
&gst_base_ts_mux_src_factory, GST_TYPE_AGGREGATOR_PAD);
}
mux->prog_map = NULL;
mux->alignment = BASETSMUX_DEFAULT_ALIGNMENT;
mux->bitrate = TSMUX_DEFAULT_BITRATE;
+ mux->scte35_pid = DEFAULT_SCTE_35_PID;
+ mux->scte35_null_interval = TSMUX_DEFAULT_SCTE_35_NULL_INTERVAL;
mux->packet_size = GST_BASE_TS_MUX_NORMAL_PACKET_LENGTH;
mux->automatic_alignment = 0;
guint si_interval;
guint64 bitrate;
guint pcr_interval;
-
+ guint scte35_pid;
+ guint scte35_null_interval;
+
/* state */
gboolean first;
GstClockTime pending_key_unit_ts;
GstEvent *force_key_unit_event;
+ GstMpegtsSection *pending_scte35_section;
/* write callback handling/state */
GstFlowReturn last_flow_ret;
static gboolean tsmux_write_pat (TsMux * mux);
static gboolean tsmux_write_pmt (TsMux * mux, TsMuxProgram * program);
+static gboolean tsmux_write_scte_null (TsMux * mux, TsMuxProgram * program);
static void
tsmux_section_free (TsMuxSection * section)
{
return TRUE;
}
+
/**
* tsmux_free:
* @mux: a #TsMux
program->pmt_pid = mux->next_pmt_pid++;
program->pcr_stream = NULL;
+ /* SCTE35 is disabled by default */
+ program->scte35_pid = 0;
+ program->scte35_null_interval = TSMUX_DEFAULT_SCTE_35_NULL_INTERVAL;
+ program->next_scte35_pcr = -1;
+
program->streams = g_array_sized_new (FALSE, TRUE, sizeof (TsMuxStream *), 1);
mux->programs = g_list_prepend (mux->programs, program);
}
/**
+ * tsmux_program_set_scte35_interval:
+ * @program: a #TsMuxProgram
+ * @freq: a new SCTE-35 NULL interval
+ *
+ * Set the interval (in cycles of the 90kHz clock) for sending out the SCTE-35
+ * NULL command. This is only effective is the SCTE-35 PID is not 0.
+ */
+void
+tsmux_program_set_scte35_interval (TsMuxProgram * program, guint interval)
+{
+ g_return_if_fail (program != NULL);
+
+ program->scte35_null_interval = interval;
+}
+
+/**
* tsmux_resend_pmt:
* @program: a #TsMuxProgram
*
}
/**
+ * tsmux_program_set_scte35_pid:
+ * @program: a #TsMuxProgram
+ * @pid: The pid to use, or 0 to deactivate usage.
+ *
+ * Set the @pid to use for sending SCTE-35 packets on the given
+ * @program.
+ *
+ * This needs to be called as early as possible if SCTE-35 sections
+ * are even going to be used with the given @program so that the PMT
+ * can be properly configured.
+ */
+void
+tsmux_program_set_scte35_pid (TsMuxProgram * program, guint16 pid)
+{
+ TsMuxSection *section;
+ GstMpegtsSCTESIT *sit;
+ g_return_if_fail (program != NULL);
+
+ program->scte35_pid = pid;
+ /* Create/Update the section */
+ if (program->scte35_null_section) {
+ tsmux_section_free (program->scte35_null_section);
+ program->scte35_null_section = NULL;
+ }
+ if (pid != 0) {
+ program->scte35_null_section = section = g_slice_new0 (TsMuxSection);
+ section->pi.pid = pid;
+ sit = gst_mpegts_scte_null_new ();
+ section->section = gst_mpegts_section_from_scte_sit (sit, pid);
+ }
+}
+
+/**
+ * tsmux_program_get_scte35_pid:
+ * @program: a #TsMuxProgram
+ *
+ * Get the PID configured for sending SCTE-35 packets.
+ *
+ * Returns: the configured SCTE-35 PID, or 0 if not active.
+ */
+guint16
+tsmux_program_get_scte35_pid (TsMuxProgram * program)
+{
+ g_return_val_if_fail (program != NULL, 0);
+
+ return program->scte35_pid;
+}
+
+/**
* tsmux_program_add_stream:
* @program: a #TsMuxProgram
* @stream: a #TsMuxStream
return TRUE;
}
+/* The unused_arg is needed for g_hash_table_foreach() */
static gboolean
-tsmux_section_write_packet (GstMpegtsSectionType * type,
+tsmux_section_write_packet (gpointer unused_arg,
TsMuxSection * section, TsMux * mux)
{
GstBuffer *section_buffer;
return FALSE;
}
+/**
+ * tsmux_send_section:
+ * @mux: a #TsMux
+ * @section: (transfer full): a #GstMpegtsSection to add
+ *
+ * Send a @section immediately on the stream.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ */
+gboolean
+tsmux_send_section (TsMux * mux, GstMpegtsSection * section)
+{
+ gboolean ret;
+ TsMuxSection tsmux_section;
+
+ g_return_val_if_fail (mux != NULL, FALSE);
+ g_return_val_if_fail (section != NULL, FALSE);
+
+ memset (&tsmux_section, 0, sizeof (tsmux_section));
+
+ GST_DEBUG ("Sending mpegts section with type %d to mux",
+ section->section_type);
+
+ tsmux_section.section = section;
+ tsmux_section.pi.pid = section->pid;
+
+ ret = tsmux_section_write_packet (NULL, &tsmux_section, mux);
+ gst_mpegts_section_unref (section);
+
+ return ret;
+}
+
static gboolean
tsmux_write_si (TsMux * mux)
{
cur_pcr = get_current_pcr (mux, cur_ts);
}
+
+ if (program->scte35_pid != 0) {
+ gboolean write_scte_null = FALSE;
+ if (program->next_scte35_pcr == -1)
+ write_scte_null = TRUE;
+ else if (cur_pcr > program->next_scte35_pcr)
+ write_scte_null = TRUE;
+
+ if (write_scte_null) {
+ GST_DEBUG ("next scte35 pcr %" G_GINT64_FORMAT,
+ program->next_scte35_pcr);
+ if (program->next_scte35_pcr == -1)
+ program->next_scte35_pcr =
+ cur_pcr + program->scte35_null_interval * 300;
+ else
+ program->next_scte35_pcr += program->scte35_null_interval * 300;
+ GST_DEBUG ("next scte35 NOW pcr %" G_GINT64_FORMAT,
+ program->next_scte35_pcr);
+
+ if (!tsmux_write_scte_null (mux, program))
+ return FALSE;
+
+ cur_pcr = get_current_pcr (mux, cur_ts);
+ }
+ }
}
return TRUE;
/* Free PMT section */
if (program->pmt.section)
gst_mpegts_section_unref (program->pmt.section);
+ if (program->scte35_null_section)
+ tsmux_section_free (program->scte35_null_section);
g_array_free (program->streams, TRUE);
g_slice_free (TsMuxProgram, program);
mux->pat_changed = FALSE;
}
- return tsmux_section_write_packet (GINT_TO_POINTER (GST_MPEGTS_SECTION_PAT),
- &mux->pat, mux);
+ return tsmux_section_write_packet (NULL, &mux->pat, mux);
}
static gboolean
g_ptr_array_add (pmt->descriptors, descriptor);
#endif
+ /* Will SCTE-35 be potentially used ? */
+ if (program->scte35_pid != 0) {
+ descriptor = gst_mpegts_descriptor_from_registration ("CUEI", NULL, 0);
+ g_ptr_array_add (pmt->descriptors, descriptor);
+ }
+
/* Write out the entries */
for (i = 0; i < program->streams->len; i++) {
GstMpegtsPMTStream *pmt_stream;
g_ptr_array_add (pmt->streams, pmt_stream);
}
+ /* Will SCTE-35 be potentially used ? */
+ if (program->scte35_pid != 0) {
+ GstMpegtsPMTStream *pmt_stream = gst_mpegts_pmt_stream_new ();
+ pmt_stream->stream_type = GST_MPEGTS_STREAM_TYPE_SCTE_SIT;
+ pmt_stream->pid = program->scte35_pid;
+ g_ptr_array_add (pmt->streams, pmt_stream);
+ }
+
TS_DEBUG ("PMT for program %d has %d streams",
program->pgm_number, program->streams->len);
program->pmt.section->version_number = program->pmt_version++;
}
- return tsmux_section_write_packet (GINT_TO_POINTER (GST_MPEGTS_SECTION_PMT),
- &program->pmt, mux);
+ return tsmux_section_write_packet (NULL, &program->pmt, mux);
+}
+
+static gboolean
+tsmux_write_scte_null (TsMux * mux, TsMuxProgram * program)
+{
+ /* SCTE-35 NULL section is created when PID is set */
+ GST_LOG ("Writing SCTE NULL packet");
+ return tsmux_section_write_packet (NULL, program->scte35_null_section, mux);
}
void
/* PID to write the PMT */
guint16 pmt_pid;
+ TsMuxSection *scte35_null_section;
+ /* SCTE-35 pid (0 if inactive/unused) */
+ guint16 scte35_pid;
+ /* Interval between SCTE-35 NULL packets in MPEG PTS clock time */
+ guint scte35_null_interval;
+ /* Next SCTE-35 position, 27 MHz */
+ gint64 next_scte35_pcr;
+
/* stream which carries the PCR */
TsMuxStream *pcr_stream;
void tsmux_set_pmt_interval (TsMuxProgram *program, guint interval);
guint tsmux_get_pmt_interval (TsMuxProgram *program);
void tsmux_resend_pmt (TsMuxProgram *program);
+void tsmux_program_set_scte35_pid (TsMuxProgram *program, guint16 pid);
+guint16 tsmux_program_get_scte35_pid (TsMuxProgram *program);
+void tsmux_program_set_scte35_interval (TsMuxProgram *mux, guint interval);
+
/* SI table management */
void tsmux_set_si_interval (TsMux *mux, guint interval);
void tsmux_resend_si (TsMux *mux);
gboolean tsmux_add_mpegts_si_section (TsMux * mux, GstMpegtsSection * section);
+/* One-time sections */
+gboolean tsmux_send_section (TsMux *mux, GstMpegtsSection *section);
+
/* stream management */
TsMuxStream * tsmux_create_stream (TsMux *mux, guint stream_type, guint16 pid, gchar *language);
TsMuxStream * tsmux_find_stream (TsMux *mux, guint16 pid);
#define TSMUX_DEFAULT_SI_INTERVAL (TSMUX_CLOCK_FREQ / 10)
/* PCR interval (1/25th sec) */
#define TSMUX_DEFAULT_PCR_INTERVAL (TSMUX_CLOCK_FREQ / 25)
+/* SCTE-35 NULL Interval (5mins) */
+#define TSMUX_DEFAULT_SCTE_35_NULL_INTERVAL (TSMUX_CLOCK_FREQ * 300)
/* Bitrate (bits per second) */
#define TSMUX_DEFAULT_BITRATE 0
-foreach fname : ['ts-parser.c', 'ts-section-writer.c']
+foreach fname : ['ts-parser.c', 'ts-section-writer.c', 'ts-scte-writer.c']
exe_name = fname.split('.').get(0).underscorify()
executable(exe_name,
--- /dev/null
+#include <gst/gst.h>
+#include <gst/mpegts/mpegts.h>
+
+/* 45s stream
+ * Send scte-35 NULL packets every 5s
+ * Use PID 123 for SCTE-35 */
+#define PIPELINE_STR "videotestsrc is-live=True num-buffers=1350 ! video/x-raw,framerate=30/1 ! x264enc tune=zerolatency ! queue ! mpegtsmux name=mux scte-35-pid=123 scte-35-null-interval=450000 ! filesink location=test-scte.ts"
+
+static void
+_on_bus_message (GstBus * bus, GstMessage * message, GMainLoop * mainloop)
+{
+ switch (GST_MESSAGE_TYPE (message)) {
+ case GST_MESSAGE_ERROR:
+ case GST_MESSAGE_EOS:
+ g_main_loop_quit (mainloop);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+send_splice (GstElement * mux, gboolean out)
+{
+ GstMpegtsSCTESIT *sit;
+ GstMpegtsSection *section;
+
+ g_print ("Sending Splice %s event\n", out ? "Out" : "In");
+
+ /* Splice is at 5s for 30s */
+ if (out)
+ sit = gst_mpegts_scte_splice_out_new (1, 5 * 90000, 30 * 90000);
+ else
+ sit = gst_mpegts_scte_splice_in_new (2, 35 * 90000);
+
+ section = gst_mpegts_section_from_scte_sit (sit, 123);
+ gst_mpegts_section_send_event (section, mux);
+ gst_mpegts_section_unref (section);
+}
+
+static gboolean
+send_splice_in (GstElement * mux)
+{
+ send_splice (mux, FALSE);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+send_splice_out (GstElement * mux)
+{
+ send_splice (mux, TRUE);
+
+ /* In 30s send the splice-in one */
+ g_timeout_add_seconds (30, (GSourceFunc) send_splice_in, mux);
+
+ return G_SOURCE_REMOVE;
+}
+
+int
+main (int argc, char **argv)
+{
+ GstElement *pipeline = NULL;
+ GError *error = NULL;
+ GstBus *bus;
+ GMainLoop *mainloop;
+ GstElement *mux;
+
+ gst_init (&argc, &argv);
+ gst_mpegts_initialize ();
+
+ pipeline = gst_parse_launch (PIPELINE_STR, &error);
+ if (error) {
+ g_print ("pipeline could not be constructed: %s\n", error->message);
+ g_clear_error (&error);
+ return 1;
+ }
+
+ mainloop = g_main_loop_new (NULL, FALSE);
+
+ mux = gst_bin_get_by_name (GST_BIN (pipeline), "mux");
+ /* Send splice-out 1s in */
+ g_timeout_add_seconds (1, (GSourceFunc) send_splice_out, mux);
+ gst_object_unref (mux);
+
+ /* Put a bus handler */
+ bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
+ gst_bus_add_signal_watch (bus);
+ g_signal_connect (bus, "message", (GCallback) _on_bus_message, mainloop);
+
+ /* Start pipeline */
+ gst_element_set_state (pipeline, GST_STATE_PLAYING);
+ g_main_loop_run (mainloop);
+
+ gst_element_set_state (pipeline, GST_STATE_NULL);
+
+ gst_object_unref (pipeline);
+ gst_object_unref (bus);
+
+ return 0;
+}