mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-26 11:41:09 +00:00
splitmuxsink: Add fragment offset and duration to message
Publish the playback offset for and duration into the splitmuxsink-fragment-closed bus message as each fragment finishes. These can be passed to splitmuxsrc via the 'add-fragment' signal to avoid splitmuxsrc measuring all files on startup Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7053>
This commit is contained in:
parent
b0df6ee408
commit
ed03e8f8ab
6 changed files with 446 additions and 53 deletions
|
@ -183,7 +183,7 @@ GST_STATIC_PAD_TEMPLATE ("caption_%u",
|
|||
|
||||
static GQuark PAD_CONTEXT;
|
||||
static GQuark EOS_FROM_US;
|
||||
static GQuark RUNNING_TIME;
|
||||
static GQuark SINK_FRAGMENT_INFO;
|
||||
/* EOS_FROM_US is only valid in async-finalize mode. We need to know whether
|
||||
* to forward an incoming EOS message, but we cannot rely on the state of the
|
||||
* splitmux anymore, so we set this qdata on the sink instead.
|
||||
|
@ -203,9 +203,10 @@ static GQuark RUNNING_TIME;
|
|||
static void
|
||||
_do_init (void)
|
||||
{
|
||||
PAD_CONTEXT = g_quark_from_static_string ("pad-context");
|
||||
EOS_FROM_US = g_quark_from_static_string ("eos-from-us");
|
||||
RUNNING_TIME = g_quark_from_static_string ("running-time");
|
||||
PAD_CONTEXT = g_quark_from_static_string ("splitmuxsink-pad-context");
|
||||
EOS_FROM_US = g_quark_from_static_string ("splitmuxsink-eos-from-us");
|
||||
SINK_FRAGMENT_INFO =
|
||||
g_quark_from_static_string ("splitmuxsink-fragment-info");
|
||||
GST_DEBUG_CATEGORY_INIT (splitmux_debug, "splitmuxsink", 0,
|
||||
"Split File Muxing Sink");
|
||||
}
|
||||
|
@ -1079,7 +1080,8 @@ mq_stream_ctx_reset (MqStreamCtx * ctx)
|
|||
{
|
||||
gst_segment_init (&ctx->in_segment, GST_FORMAT_UNDEFINED);
|
||||
gst_segment_init (&ctx->out_segment, GST_FORMAT_UNDEFINED);
|
||||
ctx->in_running_time = ctx->out_running_time = GST_CLOCK_STIME_NONE;
|
||||
ctx->out_fragment_start_runts = ctx->in_running_time = ctx->out_running_time =
|
||||
GST_CLOCK_STIME_NONE;
|
||||
g_queue_foreach (&ctx->queued_bufs, (GFunc) mq_stream_buf_free, NULL);
|
||||
g_queue_clear (&ctx->queued_bufs);
|
||||
}
|
||||
|
@ -1125,33 +1127,46 @@ send_fragment_opened_closed_msg (GstSplitMuxSink * splitmux, gboolean opened,
|
|||
GstElement * sink)
|
||||
{
|
||||
gchar *location = NULL;
|
||||
GstMessage *msg;
|
||||
const gchar *msg_name = opened ?
|
||||
"splitmuxsink-fragment-opened" : "splitmuxsink-fragment-closed";
|
||||
GstClockTime running_time = splitmux->reference_ctx->out_running_time;
|
||||
OutputFragmentInfo *out_fragment_info = &splitmux->out_fragment_info;
|
||||
|
||||
if (!opened) {
|
||||
GstClockTime *rtime = g_object_get_qdata (G_OBJECT (sink), RUNNING_TIME);
|
||||
if (rtime)
|
||||
running_time = *rtime;
|
||||
OutputFragmentInfo *sink_fragment_info =
|
||||
g_object_get_qdata (G_OBJECT (sink), SINK_FRAGMENT_INFO);
|
||||
if (sink_fragment_info != NULL) {
|
||||
out_fragment_info = sink_fragment_info;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_object_class_find_property (G_OBJECT_GET_CLASS (sink),
|
||||
"location") != NULL)
|
||||
"location") != NULL) {
|
||||
g_object_get (sink, "location", &location, NULL);
|
||||
}
|
||||
|
||||
GST_DEBUG_OBJECT (splitmux,
|
||||
"Sending %s message. Running time %" GST_TIME_FORMAT " location %s",
|
||||
msg_name, GST_TIME_ARGS (running_time), GST_STR_NULL (location));
|
||||
msg_name, GST_TIME_ARGS (out_fragment_info->last_running_time),
|
||||
GST_STR_NULL (location));
|
||||
|
||||
/* If it's in the middle of a teardown, the reference_ctc might have become
|
||||
* NULL */
|
||||
if (splitmux->reference_ctx) {
|
||||
msg = gst_message_new_element (GST_OBJECT (splitmux),
|
||||
gst_structure_new (msg_name,
|
||||
"location", G_TYPE_STRING, location,
|
||||
"running-time", GST_TYPE_CLOCK_TIME, running_time,
|
||||
"sink", GST_TYPE_ELEMENT, sink, NULL));
|
||||
GstStructure *s = gst_structure_new (msg_name,
|
||||
"location", G_TYPE_STRING, location,
|
||||
"running-time", GST_TYPE_CLOCK_TIME,
|
||||
out_fragment_info->last_running_time, "sink", GST_TYPE_ELEMENT,
|
||||
sink, NULL);
|
||||
|
||||
if (!opened) {
|
||||
GstClockTime offset = out_fragment_info->fragment_offset;
|
||||
GstClockTime duration = out_fragment_info->fragment_duration;
|
||||
|
||||
gst_structure_set (s,
|
||||
"fragment-offset", GST_TYPE_CLOCK_TIME, offset,
|
||||
"fragment-duration", GST_TYPE_CLOCK_TIME, duration, NULL);
|
||||
}
|
||||
GstMessage *msg = gst_message_new_element (GST_OBJECT (splitmux), s);
|
||||
gst_element_post_message (GST_ELEMENT_CAST (splitmux), msg);
|
||||
}
|
||||
|
||||
|
@ -1250,6 +1265,43 @@ all_contexts_are_async_eos (GstSplitMuxSink * splitmux)
|
|||
return ret;
|
||||
}
|
||||
|
||||
/* Called with splitmux lock held before ending a fragment,
|
||||
* to update the fragment info used for sending fragment opened/closed messages */
|
||||
static void
|
||||
update_output_fragment_info (GstSplitMuxSink * splitmux)
|
||||
{
|
||||
// Update the fragment output info before finalization
|
||||
|
||||
GstClockTime offset =
|
||||
splitmux->out_fragment_start_runts - splitmux->out_start_runts;
|
||||
|
||||
GstClockTime duration = GST_CLOCK_TIME_NONE;
|
||||
|
||||
/* Look for the largest duration across all streams */
|
||||
for (GList * item = splitmux->contexts; item; item = item->next) {
|
||||
MqStreamCtx *ctx = item->data;
|
||||
if (ctx->out_running_time_end > splitmux->out_fragment_start_runts) {
|
||||
GstClockTime ctx_duration =
|
||||
ctx->out_running_time_end - splitmux->out_fragment_start_runts;
|
||||
if (!GST_CLOCK_TIME_IS_VALID (duration) || ctx_duration > duration) {
|
||||
duration = ctx_duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
"Updating fragment info with reference TS %" GST_STIME_FORMAT
|
||||
" with fragment-offset %" GST_TIMEP_FORMAT
|
||||
" and fragment-duration %" GST_TIMEP_FORMAT,
|
||||
GST_STIME_ARGS (splitmux->reference_ctx->out_running_time),
|
||||
&offset, &duration);
|
||||
|
||||
splitmux->out_fragment_info.last_running_time =
|
||||
splitmux->reference_ctx->out_running_time;
|
||||
splitmux->out_fragment_info.fragment_offset = offset;
|
||||
splitmux->out_fragment_info.fragment_duration = duration;
|
||||
}
|
||||
|
||||
/* Called with splitmux lock held to check if this output
|
||||
* context needs to sleep to wait for the release of the
|
||||
* next GOP, or to send EOS to close out the current file
|
||||
|
@ -1286,6 +1338,8 @@ complete_or_wait_on_out (GstSplitMuxSink * splitmux, MqStreamCtx * ctx)
|
|||
GST_STIME_ARGS (my_max_out_running_time));
|
||||
|
||||
if (can_output) {
|
||||
/* Always outputting everything up to the next max_out_running_time
|
||||
* before advancing the state machine */
|
||||
if (splitmux->max_out_running_time != GST_CLOCK_STIME_NONE &&
|
||||
ctx->out_running_time < my_max_out_running_time) {
|
||||
return GST_FLOW_OK;
|
||||
|
@ -1303,14 +1357,18 @@ complete_or_wait_on_out (GstSplitMuxSink * splitmux, MqStreamCtx * ctx)
|
|||
case SPLITMUX_OUTPUT_STATE_ENDING_STREAM:
|
||||
/* We've reached the max out running_time to get here, so end this file now */
|
||||
if (ctx->out_eos == FALSE) {
|
||||
update_output_fragment_info (splitmux);
|
||||
|
||||
if (splitmux->async_finalize) {
|
||||
/* For async finalization, we must store the fragment timing
|
||||
* info on the element via qdata, because EOS will be processed
|
||||
* asynchronously */
|
||||
GstClockTime *sink_running_time = g_new (GstClockTime, 1);
|
||||
*sink_running_time = splitmux->reference_ctx->out_running_time;
|
||||
|
||||
OutputFragmentInfo *sink_fragment_info =
|
||||
g_new (OutputFragmentInfo, 1);
|
||||
*sink_fragment_info = splitmux->out_fragment_info;
|
||||
g_object_set_qdata_full (G_OBJECT (splitmux->sink),
|
||||
RUNNING_TIME, sink_running_time, g_free);
|
||||
SINK_FRAGMENT_INFO, sink_fragment_info, g_free);
|
||||
|
||||
/* We must set EOS asynchronously at this point. We cannot defer
|
||||
* it, because we need all contexts to wake up, for the
|
||||
|
@ -1333,9 +1391,15 @@ complete_or_wait_on_out (GstSplitMuxSink * splitmux, MqStreamCtx * ctx)
|
|||
send_eos (splitmux, ctx);
|
||||
continue;
|
||||
}
|
||||
} else if (splitmux->output_state ==
|
||||
SPLITMUX_OUTPUT_STATE_ENDING_STREAM) {
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
"At end-of-stream state, and context %p is already EOS. Returning.",
|
||||
ctx);
|
||||
return GST_FLOW_OK;
|
||||
} else {
|
||||
GST_INFO_OBJECT (splitmux,
|
||||
"At end-of-file state, but context %p is already EOS", ctx);
|
||||
"At end-of-file state, and context %p is already EOS.", ctx);
|
||||
}
|
||||
break;
|
||||
case SPLITMUX_OUTPUT_STATE_START_NEXT_FILE:
|
||||
|
@ -1721,18 +1785,23 @@ handle_mq_output (GstPad * pad, GstPadProbeInfo * info, MqStreamCtx * ctx)
|
|||
locked = TRUE;
|
||||
if (splitmux->output_state == SPLITMUX_OUTPUT_STATE_STOPPED)
|
||||
goto beach;
|
||||
|
||||
GST_INFO_OBJECT (splitmux,
|
||||
"Have EOS event at pad %" GST_PTR_FORMAT " ctx %p", pad, ctx);
|
||||
ctx->out_eos = TRUE;
|
||||
|
||||
if (ctx == splitmux->reference_ctx) {
|
||||
GST_INFO_OBJECT (splitmux,
|
||||
"EOS on reference context - ending the recording");
|
||||
splitmux->output_state = SPLITMUX_OUTPUT_STATE_ENDING_STREAM;
|
||||
update_output_fragment_info (splitmux);
|
||||
|
||||
// Waiting before outputting will ensure the muxer end-of-stream
|
||||
// qdata is set without racing against this EOS event reaching the muxer
|
||||
wait = TRUE;
|
||||
GST_SPLITMUX_BROADCAST_OUTPUT (splitmux);
|
||||
}
|
||||
|
||||
GST_INFO_OBJECT (splitmux,
|
||||
"Have EOS event at pad %" GST_PTR_FORMAT " ctx %p", pad, ctx);
|
||||
break;
|
||||
case GST_EVENT_GAP:{
|
||||
GstClockTime gap_ts;
|
||||
|
@ -1822,6 +1891,7 @@ handle_mq_output (GstPad * pad, GstPadProbeInfo * info, MqStreamCtx * ctx)
|
|||
"New caps were not accepted. Switching output file");
|
||||
if (ctx->out_eos == FALSE) {
|
||||
splitmux->output_state = SPLITMUX_OUTPUT_STATE_ENDING_FILE;
|
||||
update_output_fragment_info (splitmux);
|
||||
GST_SPLITMUX_BROADCAST_OUTPUT (splitmux);
|
||||
}
|
||||
}
|
||||
|
@ -1871,6 +1941,7 @@ handle_mq_output (GstPad * pad, GstPadProbeInfo * info, MqStreamCtx * ctx)
|
|||
splitmux->queued_keyframes--;
|
||||
|
||||
ctx->out_running_time = buf_info->run_ts;
|
||||
|
||||
ctx->cur_out_buffer = gst_pad_probe_info_get_buffer (info);
|
||||
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
|
@ -1884,9 +1955,56 @@ handle_mq_output (GstPad * pad, GstPadProbeInfo * info, MqStreamCtx * ctx)
|
|||
|
||||
splitmux->muxed_out_bytes += buf_info->buf_size;
|
||||
|
||||
if (GST_CLOCK_STIME_IS_VALID (buf_info->run_ts)) {
|
||||
if (!GST_CLOCK_STIME_IS_VALID (ctx->out_fragment_start_runts)) {
|
||||
ctx->out_fragment_start_runts = buf_info->run_ts;
|
||||
|
||||
/* For the first fragment check if this is the earliest of all start running times */
|
||||
if (splitmux->fragment_id == 1) {
|
||||
if (!GST_CLOCK_STIME_IS_VALID (splitmux->out_start_runts)
|
||||
|| (ctx->out_fragment_start_runts < splitmux->out_start_runts)) {
|
||||
splitmux->out_start_runts = ctx->out_fragment_start_runts;
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
"Overall recording start TS now %" GST_STIMEP_FORMAT,
|
||||
&splitmux->out_start_runts);
|
||||
}
|
||||
}
|
||||
|
||||
if (!GST_CLOCK_STIME_IS_VALID (splitmux->out_fragment_start_runts)
|
||||
|| (ctx->out_fragment_start_runts <
|
||||
splitmux->out_fragment_start_runts)) {
|
||||
splitmux->out_fragment_start_runts = ctx->out_fragment_start_runts;
|
||||
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
"Overall fragment start TS now %" GST_STIMEP_FORMAT,
|
||||
&splitmux->out_fragment_start_runts);
|
||||
}
|
||||
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
"Pad %" GST_PTR_FORMAT " buffer run TS %" GST_STIME_FORMAT
|
||||
" is first for this fragment", pad,
|
||||
GST_STIME_ARGS (ctx->out_fragment_start_runts));
|
||||
}
|
||||
|
||||
/* Extend the context end running time if it grew */
|
||||
GstClockTime end_run_ts = buf_info->run_ts;
|
||||
if (GST_CLOCK_TIME_IS_VALID (buf_info->duration)) {
|
||||
end_run_ts += buf_info->duration;
|
||||
}
|
||||
if (!GST_CLOCK_TIME_IS_VALID (ctx->out_running_time_end) ||
|
||||
end_run_ts > ctx->out_running_time_end) {
|
||||
ctx->out_running_time_end = end_run_ts;
|
||||
|
||||
GstClockTimeDiff duration = end_run_ts - ctx->out_fragment_start_runts;
|
||||
GST_LOG_OBJECT (splitmux,
|
||||
"Pad %" GST_PTR_FORMAT " fragment duration now %" GST_STIMEP_FORMAT,
|
||||
pad, &duration);
|
||||
}
|
||||
}
|
||||
#ifndef GST_DISABLE_GST_DEBUG
|
||||
{
|
||||
GstBuffer *buf = gst_pad_probe_info_get_buffer (info);
|
||||
|
||||
GST_LOG_OBJECT (pad, "Returning to pass buffer %" GST_PTR_FORMAT
|
||||
" run ts %" GST_STIME_FORMAT, buf,
|
||||
GST_STIME_ARGS (ctx->out_running_time));
|
||||
|
@ -1947,6 +2065,7 @@ restart_context (MqStreamCtx * ctx, GstSplitMuxSink * splitmux)
|
|||
/* Clear EOS flag if not actually EOS */
|
||||
ctx->out_eos = GST_PAD_IS_EOS (ctx->srcpad);
|
||||
ctx->out_eos_async_done = ctx->out_eos;
|
||||
ctx->out_fragment_start_runts = GST_CLOCK_STIME_NONE;
|
||||
|
||||
gst_object_unref (peer);
|
||||
}
|
||||
|
@ -2162,6 +2281,7 @@ start_next_fragment (GstSplitMuxSink * splitmux, MqStreamCtx * ctx)
|
|||
GST_SPLITMUX_LOCK (splitmux);
|
||||
set_next_filename (splitmux, ctx);
|
||||
splitmux->muxed_out_bytes = 0;
|
||||
splitmux->out_fragment_start_runts = GST_CLOCK_STIME_NONE;
|
||||
GST_SPLITMUX_UNLOCK (splitmux);
|
||||
|
||||
if (gst_element_set_state (sink,
|
||||
|
@ -3595,6 +3715,7 @@ gst_splitmux_sink_request_new_pad (GstElement * element,
|
|||
GST_DEBUG_OBJECT (splitmux, "splitmuxsink pad %" GST_PTR_FORMAT
|
||||
" feeds queue pad %" GST_PTR_FORMAT, ret, q_sink);
|
||||
|
||||
ctx->ctx_id = g_list_length (splitmux->contexts);
|
||||
splitmux->contexts = g_list_append (splitmux->contexts, ctx);
|
||||
|
||||
g_free (gname);
|
||||
|
@ -4008,6 +4129,9 @@ gst_splitmux_sink_reset (GstSplitMuxSink * splitmux)
|
|||
|
||||
g_queue_foreach (&splitmux->out_cmd_q, (GFunc) out_cmd_buf_free, NULL);
|
||||
g_queue_clear (&splitmux->out_cmd_q);
|
||||
|
||||
splitmux->out_fragment_start_runts = splitmux->out_start_runts =
|
||||
GST_CLOCK_STIME_NONE;
|
||||
}
|
||||
|
||||
static GstStateChangeReturn
|
||||
|
|
|
@ -68,6 +68,14 @@ typedef struct _SplitMuxOutputCommand
|
|||
} release_gop;
|
||||
} SplitMuxOutputCommand;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GstClockTime last_running_time;
|
||||
|
||||
GstClockTime fragment_offset;
|
||||
GstClockTime fragment_duration;
|
||||
} OutputFragmentInfo;
|
||||
|
||||
typedef struct _MqStreamBuf
|
||||
{
|
||||
gboolean keyframe;
|
||||
|
@ -99,6 +107,7 @@ typedef struct {
|
|||
typedef struct _MqStreamCtx
|
||||
{
|
||||
GstSplitMuxSink *splitmux;
|
||||
guint ctx_id;
|
||||
|
||||
guint q_overrun_id;
|
||||
guint sink_pad_block_id;
|
||||
|
@ -116,9 +125,12 @@ typedef struct _MqStreamCtx
|
|||
|
||||
GstSegment in_segment;
|
||||
GstSegment out_segment;
|
||||
GstClockTimeDiff out_fragment_start_runts;
|
||||
|
||||
GstClockTimeDiff in_running_time;
|
||||
|
||||
GstClockTimeDiff out_running_time;
|
||||
GstClockTimeDiff out_running_time_end; /* max run ts + durations */
|
||||
|
||||
GstElement *q;
|
||||
GQueue queued_bufs;
|
||||
|
@ -128,6 +140,7 @@ typedef struct _MqStreamCtx
|
|||
|
||||
GstBuffer *cur_out_buffer;
|
||||
GstEvent *pending_gap;
|
||||
|
||||
} MqStreamCtx;
|
||||
|
||||
struct _GstSplitMuxSink
|
||||
|
@ -183,11 +196,11 @@ struct _GstSplitMuxSink
|
|||
* stream in this fragment */
|
||||
guint64 fragment_reference_bytes;
|
||||
|
||||
/* Minimum start time (PTS or DTS) of the current fragment */
|
||||
/* Minimum start time (PTS or DTS) of the current fragment (reference stream, input side) */
|
||||
GstClockTimeDiff fragment_start_time;
|
||||
/* Start time (PTS) of the current fragment */
|
||||
/* Start time (PTS) of the current fragment (reference stream, input side) */
|
||||
GstClockTimeDiff fragment_start_time_pts;
|
||||
/* Minimum start timecode of the current fragment */
|
||||
/* Minimum start timecode of the current fragment (reference stream, input side) */
|
||||
GstVideoTimeCode *fragment_start_tc;
|
||||
|
||||
/* Oldest GOP at head, newest GOP at tail */
|
||||
|
@ -200,6 +213,12 @@ struct _GstSplitMuxSink
|
|||
|
||||
SplitMuxOutputState output_state;
|
||||
GstClockTimeDiff max_out_running_time;
|
||||
OutputFragmentInfo out_fragment_info;
|
||||
|
||||
/* Track the earliest running time (across all inputs) for the first fragment */
|
||||
GstClockTimeDiff out_start_runts;
|
||||
/* Track the earliest running time (across all inputs) for the *current* fragment */
|
||||
GstClockTimeDiff out_fragment_start_runts;
|
||||
|
||||
guint64 muxed_out_bytes;
|
||||
|
||||
|
|
|
@ -103,19 +103,63 @@ dump_error (GstMessage * msg)
|
|||
}
|
||||
|
||||
static GstMessage *
|
||||
run_pipeline (GstElement * pipeline)
|
||||
run_pipeline (GstElement * pipeline, guint num_fragments_expected,
|
||||
const GstClockTime * fragment_offsets,
|
||||
const GstClockTime * fragment_durations)
|
||||
{
|
||||
GstBus *bus = gst_element_get_bus (GST_ELEMENT (pipeline));
|
||||
GstMessage *msg;
|
||||
guint fragment_number = 0;
|
||||
|
||||
gst_element_set_state (pipeline, GST_STATE_PLAYING);
|
||||
msg = gst_bus_poll (bus, GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1);
|
||||
do {
|
||||
msg =
|
||||
gst_bus_poll (bus,
|
||||
GST_MESSAGE_EOS | GST_MESSAGE_ERROR | GST_MESSAGE_ELEMENT, -1);
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS
|
||||
|| GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
|
||||
break;
|
||||
}
|
||||
if (num_fragments_expected != 0) {
|
||||
// Handle element message
|
||||
const GstStructure *s = gst_message_get_structure (msg);
|
||||
if (gst_structure_has_name (s, "splitmuxsrc-fragment-info") ||
|
||||
gst_structure_has_name (s, "splitmuxsink-fragment-closed")) {
|
||||
GstClockTime fragment_offset, fragment_duration;
|
||||
fail_unless (gst_structure_get_clock_time (s, "fragment-offset",
|
||||
&fragment_offset));
|
||||
fail_unless (gst_structure_get_clock_time (s, "fragment-duration",
|
||||
&fragment_duration));
|
||||
if (fragment_offsets != NULL) {
|
||||
fail_unless (fragment_offsets[fragment_number] == fragment_offset,
|
||||
"Expected offset %" GST_TIME_FORMAT
|
||||
" for fragment %u. Got offset %" GST_TIME_FORMAT,
|
||||
GST_TIME_ARGS (fragment_offsets[fragment_number]),
|
||||
fragment_number, GST_TIME_ARGS (fragment_offset));
|
||||
}
|
||||
if (fragment_durations != NULL) {
|
||||
fail_unless (fragment_durations[fragment_number] == fragment_duration,
|
||||
"Expected duration %" GST_TIME_FORMAT
|
||||
" for fragment %u. Got duration %" GST_TIME_FORMAT,
|
||||
GST_TIME_ARGS (fragment_durations[fragment_number]),
|
||||
fragment_number, GST_TIME_ARGS (fragment_duration));
|
||||
}
|
||||
fragment_number++;
|
||||
}
|
||||
}
|
||||
gst_message_unref (msg);
|
||||
} while (TRUE);
|
||||
|
||||
gst_element_set_state (pipeline, GST_STATE_NULL);
|
||||
|
||||
gst_object_unref (bus);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
else if (num_fragments_expected != 0) {
|
||||
// Success. Check we got the expected number of fragment messages
|
||||
fail_unless (fragment_number == num_fragments_expected);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
@ -222,7 +266,9 @@ receive_sample (GstAppSink * appsink, gpointer user_data)
|
|||
|
||||
static void
|
||||
test_playback (const gchar * in_pattern, GstClockTime exp_first_time,
|
||||
GstClockTime exp_last_time, gboolean test_reverse)
|
||||
GstClockTime exp_last_time, gboolean test_reverse,
|
||||
guint num_fragments_expected, const GstClockTime * fragment_offsets,
|
||||
const GstClockTime * fragment_durations)
|
||||
{
|
||||
GstMessage *msg;
|
||||
GstElement *pipeline;
|
||||
|
@ -256,7 +302,9 @@ test_playback (const gchar * in_pattern, GstClockTime exp_first_time,
|
|||
/* test forwards */
|
||||
seek_pipeline (pipeline, 1.0, 0, -1);
|
||||
fail_unless (first_ts == GST_CLOCK_TIME_NONE);
|
||||
msg = run_pipeline (pipeline);
|
||||
msg =
|
||||
run_pipeline (pipeline, num_fragments_expected, fragment_offsets,
|
||||
fragment_durations);
|
||||
fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS);
|
||||
gst_message_unref (msg);
|
||||
|
||||
|
@ -272,7 +320,9 @@ test_playback (const gchar * in_pattern, GstClockTime exp_first_time,
|
|||
if (test_reverse) {
|
||||
/* Test backwards */
|
||||
seek_pipeline (pipeline, -1.0, 0, -1);
|
||||
msg = run_pipeline (pipeline);
|
||||
msg =
|
||||
run_pipeline (pipeline, num_fragments_expected, fragment_offsets,
|
||||
fragment_durations);
|
||||
fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS);
|
||||
gst_message_unref (msg);
|
||||
/* Check we saw the entire range of values */
|
||||
|
@ -394,7 +444,9 @@ GST_START_TEST (test_splitmuxsink)
|
|||
&location_state, NULL);
|
||||
gst_object_unref (bus);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
GstClockTime offsets[] = { 0, GST_SECOND, 2 * GST_SECOND };
|
||||
GstClockTime durations[] = { GST_SECOND, GST_SECOND, GST_SECOND };
|
||||
msg = run_pipeline (pipeline, 3, offsets, durations);
|
||||
|
||||
/* Clean up the location state */
|
||||
g_free (location_state.current_location);
|
||||
|
@ -427,7 +479,7 @@ GST_START_TEST (test_splitmuxsink)
|
|||
fail_unless (count == 3, "Expected 3 output files, got %d", count);
|
||||
|
||||
in_pattern = g_build_filename (tmpdir, "out*.ogg", NULL);
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE);
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE, 3, offsets, durations);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
@ -458,7 +510,7 @@ GST_START_TEST (test_splitmuxsink_clean_failure)
|
|||
g_object_set (sink, "sink", fakesink, NULL);
|
||||
gst_object_unref (sink);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
msg = run_pipeline (pipeline, 0, NULL, NULL);
|
||||
|
||||
fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR);
|
||||
gst_message_unref (msg);
|
||||
|
@ -501,7 +553,9 @@ GST_START_TEST (test_splitmuxsink_multivid)
|
|||
g_free (dest_pattern);
|
||||
g_object_unref (sink);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
GstClockTime offsets[] = { 0, GST_SECOND, 2 * GST_SECOND };
|
||||
GstClockTime durations[] = { GST_SECOND, GST_SECOND, GST_SECOND };
|
||||
msg = run_pipeline (pipeline, 3, offsets, durations);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -519,7 +573,7 @@ GST_START_TEST (test_splitmuxsink_multivid)
|
|||
* written, and causes test failures like buffers being output
|
||||
* multiple times by qtdemux as it loops through GOPs. Disable that
|
||||
* for now */
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, FALSE);
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, FALSE, 3, offsets, durations);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
@ -553,7 +607,9 @@ GST_START_TEST (test_splitmuxsink_async)
|
|||
g_free (dest_pattern);
|
||||
g_object_unref (sink);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
GstClockTime offsets[] = { 0, GST_SECOND, 2 * GST_SECOND };
|
||||
GstClockTime durations[] = { GST_SECOND, GST_SECOND, GST_SECOND };
|
||||
msg = run_pipeline (pipeline, 3, offsets, durations);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -583,7 +639,7 @@ GST_START_TEST (test_splitmuxsink_async)
|
|||
fail_unless (count == 3, "Expected 3 output files, got %d", count);
|
||||
|
||||
in_pattern = g_build_filename (tmpdir, "matroska*.mkv", NULL);
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE);
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE, 3, offsets, durations);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
@ -690,7 +746,7 @@ run_eos_pipeline (guint num_video_buf, guint num_audio_buf,
|
|||
|
||||
fail_if (pipeline == NULL);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
msg = run_pipeline (pipeline, 0, NULL, NULL);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -778,7 +834,7 @@ splitmuxsink_split_by_keyframe (gboolean send_keyframe_request,
|
|||
gst_object_unref (srcpad);
|
||||
gst_object_unref (enc);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
msg = run_pipeline (pipeline, 0, 0, NULL);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -810,7 +866,7 @@ splitmuxsink_split_by_keyframe (gboolean send_keyframe_request,
|
|||
* written, and causes test failures like buffers being output
|
||||
* multiple times by qtdemux as it loops through GOPs. Disable that
|
||||
* for now */
|
||||
test_playback (in_pattern, 0, 6 * GST_SECOND, FALSE);
|
||||
test_playback (in_pattern, 0, 6 * GST_SECOND, FALSE, 0, NULL, NULL);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
|
|
@ -103,19 +103,63 @@ dump_error (GstMessage * msg)
|
|||
}
|
||||
|
||||
static GstMessage *
|
||||
run_pipeline (GstElement * pipeline)
|
||||
run_pipeline (GstElement * pipeline, guint num_fragments_expected,
|
||||
const GstClockTime * fragment_offsets,
|
||||
const GstClockTime * fragment_durations)
|
||||
{
|
||||
GstBus *bus = gst_element_get_bus (GST_ELEMENT (pipeline));
|
||||
GstMessage *msg;
|
||||
guint fragment_number = 0;
|
||||
|
||||
gst_element_set_state (pipeline, GST_STATE_PLAYING);
|
||||
msg = gst_bus_poll (bus, GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1);
|
||||
do {
|
||||
msg =
|
||||
gst_bus_poll (bus,
|
||||
GST_MESSAGE_EOS | GST_MESSAGE_ERROR | GST_MESSAGE_ELEMENT, -1);
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS
|
||||
|| GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
|
||||
break;
|
||||
}
|
||||
if (num_fragments_expected != 0) {
|
||||
// Handle element message
|
||||
const GstStructure *s = gst_message_get_structure (msg);
|
||||
if (gst_structure_has_name (s, "splitmuxsrc-fragment-info") ||
|
||||
gst_structure_has_name (s, "splitmuxsink-fragment-closed")) {
|
||||
GstClockTime fragment_offset, fragment_duration;
|
||||
fail_unless (gst_structure_get_clock_time (s, "fragment-offset",
|
||||
&fragment_offset));
|
||||
fail_unless (gst_structure_get_clock_time (s, "fragment-duration",
|
||||
&fragment_duration));
|
||||
if (fragment_offsets != NULL) {
|
||||
fail_unless (fragment_offsets[fragment_number] == fragment_offset,
|
||||
"Expected offset %" GST_TIME_FORMAT
|
||||
" for fragment %u. Got offset %" GST_TIME_FORMAT,
|
||||
GST_TIME_ARGS (fragment_offsets[fragment_number]),
|
||||
fragment_number, GST_TIME_ARGS (fragment_offset));
|
||||
}
|
||||
if (fragment_durations != NULL) {
|
||||
fail_unless (fragment_durations[fragment_number] == fragment_duration,
|
||||
"Expected duration %" GST_TIME_FORMAT
|
||||
" for fragment %u. Got duration %" GST_TIME_FORMAT,
|
||||
GST_TIME_ARGS (fragment_durations[fragment_number]),
|
||||
fragment_number, GST_TIME_ARGS (fragment_duration));
|
||||
}
|
||||
fragment_number++;
|
||||
}
|
||||
}
|
||||
gst_message_unref (msg);
|
||||
} while (TRUE);
|
||||
|
||||
gst_element_set_state (pipeline, GST_STATE_NULL);
|
||||
|
||||
gst_object_unref (bus);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
else if (num_fragments_expected != 0) {
|
||||
// Success. Check we got the expected number of fragment messages
|
||||
fail_unless (fragment_number == num_fragments_expected);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
@ -216,7 +260,9 @@ receive_sample (GstAppSink * appsink, gpointer user_data G_GNUC_UNUSED)
|
|||
|
||||
static void
|
||||
test_playback (const gchar * in_pattern, GstClockTime exp_first_time,
|
||||
GstClockTime exp_last_time, gboolean test_reverse)
|
||||
GstClockTime exp_last_time, gboolean test_reverse,
|
||||
guint num_fragments_expected, const GstClockTime * fragment_offsets,
|
||||
const GstClockTime * fragment_durations)
|
||||
{
|
||||
GstMessage *msg;
|
||||
GstElement *pipeline;
|
||||
|
@ -250,7 +296,9 @@ test_playback (const gchar * in_pattern, GstClockTime exp_first_time,
|
|||
/* test forwards */
|
||||
seek_pipeline (pipeline, 1.0, 0, -1);
|
||||
fail_unless (first_ts == GST_CLOCK_TIME_NONE);
|
||||
msg = run_pipeline (pipeline);
|
||||
msg =
|
||||
run_pipeline (pipeline, num_fragments_expected, fragment_offsets,
|
||||
fragment_durations);
|
||||
fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS);
|
||||
gst_message_unref (msg);
|
||||
|
||||
|
@ -266,7 +314,9 @@ test_playback (const gchar * in_pattern, GstClockTime exp_first_time,
|
|||
if (test_reverse) {
|
||||
/* Test backwards */
|
||||
seek_pipeline (pipeline, -1.0, 0, -1);
|
||||
msg = run_pipeline (pipeline);
|
||||
msg =
|
||||
run_pipeline (pipeline, num_fragments_expected, fragment_offsets,
|
||||
fragment_durations);
|
||||
fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS);
|
||||
gst_message_unref (msg);
|
||||
/* Check we saw the entire range of values */
|
||||
|
@ -287,7 +337,10 @@ GST_START_TEST (test_splitmuxsrc)
|
|||
{
|
||||
gchar *in_pattern =
|
||||
g_build_filename (GST_TEST_FILES_PATH, "splitvideo*.ogg", NULL);
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE);
|
||||
|
||||
GstClockTime offsets[] = { 0, GST_SECOND, 2 * GST_SECOND };
|
||||
GstClockTime durations[] = { GST_SECOND, GST_SECOND, GST_SECOND };
|
||||
test_playback (in_pattern, 0, 3 * GST_SECOND, TRUE, 3, offsets, durations);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
@ -320,7 +373,7 @@ GST_START_TEST (test_splitmuxsrc_format_location)
|
|||
(GCallback) src_format_location_cb, NULL);
|
||||
g_object_unref (src);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
msg = run_pipeline (pipeline, 0, NULL, NULL);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -533,7 +586,14 @@ GST_START_TEST (test_splitmuxsrc_sparse_streams)
|
|||
(GCallback) new_sample_verify_1sec_offset, &tsink_prev_ts);
|
||||
g_clear_object (&element);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
/* Vorbis packet sizes cause some slightly strange fragment sizes */
|
||||
GstClockTime offsets[] = { 0, 999666666, 2 * (GstClockTime) 999666666,
|
||||
3 * (GstClockTime) 999666666, 4 * (GstClockTime) 999666666
|
||||
};
|
||||
GstClockTime durations[] =
|
||||
{ 1017600000, GST_SECOND, GST_SECOND, GST_SECOND, 1107200000 };
|
||||
|
||||
msg = run_pipeline (pipeline, 5, offsets, durations);
|
||||
}
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
|
@ -608,7 +668,10 @@ GST_START_TEST (test_splitmuxsrc_caps_change)
|
|||
gst_object_unref (sinkpad);
|
||||
gst_object_unref (cf);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
GstClockTime offsets[] = { 0, GST_SECOND / 2 };
|
||||
GstClockTime durations[] = { GST_SECOND / 2, GST_SECOND / 2 };
|
||||
|
||||
msg = run_pipeline (pipeline, 2, offsets, durations);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -621,7 +684,7 @@ GST_START_TEST (test_splitmuxsrc_caps_change)
|
|||
fail_unless (count == 2, "Expected 2 output files, got %d", count);
|
||||
|
||||
in_pattern = g_build_filename (tmpdir, "out*.mp4", NULL);
|
||||
test_playback (in_pattern, 0, GST_SECOND, TRUE);
|
||||
test_playback (in_pattern, 0, GST_SECOND, TRUE, 2, offsets, durations);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
@ -654,7 +717,7 @@ GST_START_TEST (test_splitmuxsrc_robust_mux)
|
|||
g_free (dest_pattern);
|
||||
g_object_unref (sink);
|
||||
|
||||
msg = run_pipeline (pipeline);
|
||||
msg = run_pipeline (pipeline, 0, NULL, NULL);
|
||||
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR)
|
||||
dump_error (msg);
|
||||
|
@ -668,7 +731,7 @@ GST_START_TEST (test_splitmuxsrc_robust_mux)
|
|||
* reserved duration property. All we care about is that the muxing didn't fail because space ran out */
|
||||
|
||||
in_pattern = g_build_filename (tmpdir, "out*.mp4", NULL);
|
||||
test_playback (in_pattern, 0, GST_SECOND, TRUE);
|
||||
test_playback (in_pattern, 0, GST_SECOND, TRUE, 0, NULL, NULL);
|
||||
g_free (in_pattern);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
executable('splitmuxsink-fragment-info', 'splitmuxsink-fragment-info.c',
|
||||
dependencies: [gst_dep],
|
||||
c_args : gst_plugins_good_args,
|
||||
include_directories : [configinc],
|
||||
install: false)
|
||||
|
||||
executable('splitmuxsrc-extract', 'splitmuxsrc-extract.c',
|
||||
dependencies: [gst_dep],
|
||||
c_args : gst_plugins_good_args,
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) 2024 Jan Schmidt <jan@centricular.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This example creates a test recording using splitmuxsink,
|
||||
* listening for the fragment-closed messages from splitmuxsink
|
||||
* and writing a CSV file with the fragment offsets and durations
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
GMainLoop *loop;
|
||||
FILE *out_csv = NULL;
|
||||
|
||||
static gboolean
|
||||
message_handler (GstBus * bus, GstMessage * message, gpointer data)
|
||||
{
|
||||
if (message->type == GST_MESSAGE_ELEMENT) {
|
||||
const GstStructure *s = gst_message_get_structure (message);
|
||||
const gchar *name = gst_structure_get_name (s);
|
||||
|
||||
if (strcmp (name, "splitmuxsink-fragment-closed") == 0) {
|
||||
const gchar *fname = gst_structure_get_string (s, "location");
|
||||
GstClockTime start_offset, duration;
|
||||
if (!gst_structure_get_uint64 (s, "fragment-offset", &start_offset) ||
|
||||
!gst_structure_get_uint64 (s, "fragment-duration", &duration)) {
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
fprintf (out_csv, "\"%s\",%" G_GUINT64_FORMAT ",%" G_GUINT64_FORMAT "\n",
|
||||
fname, start_offset, duration);
|
||||
}
|
||||
} else if (message->type == GST_MESSAGE_EOS) {
|
||||
g_main_loop_quit (loop);
|
||||
} else if (message->type == GST_MESSAGE_ERROR) {
|
||||
GError *err;
|
||||
gchar *debug_info;
|
||||
|
||||
gst_message_parse_error (message, &err, &debug_info);
|
||||
g_printerr ("Error received from element %s: %s\n",
|
||||
GST_OBJECT_NAME (message->src), err->message);
|
||||
g_printerr ("Debugging information: %s\n",
|
||||
debug_info ? debug_info : "none");
|
||||
g_main_loop_quit (loop);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
GstElement *pipe;
|
||||
GstBus *bus;
|
||||
|
||||
gst_init (&argc, &argv);
|
||||
|
||||
if (argc < 3) {
|
||||
g_printerr
|
||||
("Usage: %s target_dir out.csv\n Pass splitmuxsink target directory for generated recording, and out.csv to receive the fragment info\n",
|
||||
argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
out_csv = fopen (argv[2], "w");
|
||||
if (out_csv == NULL) {
|
||||
g_printerr ("Failed to open output file %s", argv[2]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
GError *error = NULL;
|
||||
pipe =
|
||||
gst_parse_launch
|
||||
("videotestsrc num-buffers=300 ! video/x-raw,framerate=30/1 ! timeoverlay ! x264enc key-int-max=30 ! "
|
||||
"h264parse ! queue ! splitmuxsink name=sink "
|
||||
"audiotestsrc samplesperbuffer=1600 num-buffers=300 ! audio/x-raw,rate=48000 ! opusenc ! queue ! sink.audio_0 ",
|
||||
&error);
|
||||
|
||||
if (pipe == NULL || error != NULL) {
|
||||
g_print ("Failed to create pipeline. Error %s\n", error->message);
|
||||
return 3;
|
||||
}
|
||||
|
||||
GstElement *splitmuxsink = gst_bin_get_by_name (GST_BIN (pipe), "sink");
|
||||
|
||||
/* Set the files glob on src */
|
||||
gchar *file_pattern = g_strdup_printf ("%s/test%%05d.mp4", argv[1]);
|
||||
g_object_set (splitmuxsink, "location", file_pattern, NULL);
|
||||
g_object_set (splitmuxsink, "max-size-time", GST_SECOND, NULL);
|
||||
|
||||
gst_object_unref (splitmuxsink);
|
||||
|
||||
bus = gst_element_get_bus (pipe);
|
||||
gst_bus_add_watch (bus, message_handler, NULL);
|
||||
gst_object_unref (bus);
|
||||
|
||||
gst_element_set_state (pipe, GST_STATE_PLAYING);
|
||||
|
||||
loop = g_main_loop_new (NULL, FALSE);
|
||||
g_main_loop_run (loop);
|
||||
|
||||
fclose (out_csv);
|
||||
|
||||
gst_element_set_state (pipe, GST_STATE_NULL);
|
||||
|
||||
gst_object_unref (pipe);
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Reference in a new issue