From 1d058c7d8a3c3fdb5876e97f9706dedccff29e5f Mon Sep 17 00:00:00 2001 From: Jan Schmidt Date: Wed, 1 Apr 2015 11:15:38 +1100 Subject: [PATCH] isomp4: Implement robust muxing using ping-pong strategy Implement a robust recording mode, where the output file is always in a playable state, seeking and rewriting the moov header at a configurable interval. Rewriting moov is done using reserved space at the start of the file, and a ping-pong strategy where the moov is replaced atomically so it's never invalid. Track when tags have actually changed, and don't write them into the moov unless they've changed. Clear any existing tags when re-writing them, so we can do progressive moov updating in robust recording mode. Write placeholder mdat as a free atom plus a 32-bit mdat with '0' size, which means "rest of the file" in the spec. Re-write it later to a full 64-bit extended size atom if needed. --- gst/isomp4/atoms.c | 46 +- gst/isomp4/atoms.h | 10 +- gst/isomp4/atomsrecovery.c | 2 +- gst/isomp4/gstqtmux.c | 879 +++++++++++++++++++++++++++++-------- gst/isomp4/gstqtmux.h | 47 +- 5 files changed, 782 insertions(+), 202 deletions(-) diff --git a/gst/isomp4/atoms.c b/gst/isomp4/atoms.c index 4ac76703d7..c300e6742a 100644 --- a/gst/isomp4/atoms.c +++ b/gst/isomp4/atoms.c @@ -1132,6 +1132,20 @@ atom_udta_clear (AtomUDTA * udta) atom_info_list_free (udta->entries); } +/* Clear added tags, but keep the context/flavor the same */ +void +atom_udta_clear_tags (AtomUDTA * udta) +{ + if (udta->entries) { + atom_info_list_free (udta->entries); + udta->entries = NULL; + } + if (udta->meta && udta->meta->ilst->entries) { + atom_info_list_free (udta->meta->ilst->entries); + udta->meta->ilst->entries = NULL; + } +} + static void atom_tag_data_init (AtomTagData * data) { @@ -1980,12 +1994,13 @@ atom_stco64_copy_data (AtomSTCO64 * stco64, guint8 ** buffer, guint64 * size, prop_copy_ensure_buffer (buffer, size, offset, 8 * atom_array_get_len (&stco64->entries)); for (i = 0; i < atom_array_get_len (&stco64->entries); i++) { - guint64 *value = &atom_array_index (&stco64->entries, i); + guint64 value = + atom_array_index (&stco64->entries, i) + stco64->chunk_offset; if (trunc_to_32) { - prop_copy_uint32 ((guint32) * value, buffer, size, offset); + prop_copy_uint32 ((guint32) value, buffer, size, offset); } else { - prop_copy_uint64 (*value, buffer, size, offset); + prop_copy_uint64 (value, buffer, size, offset); } } @@ -2749,6 +2764,12 @@ atom_moov_add_trak (AtomMOOV * moov, AtomTRAK * trak) atom_moov_add_trex (moov, atom_trex_new (trak)); } +guint +atom_moov_get_trak_count (AtomMOOV * moov) +{ + return g_list_length (moov->traks); +} + static guint64 atom_trak_get_duration (AtomTRAK * trak) { @@ -2821,28 +2842,27 @@ atom_moov_set_fragmented (AtomMOOV * moov, gboolean fragmented) } void -atom_stco64_chunks_add_offset (AtomSTCO64 * stco64, guint32 offset) +atom_stco64_chunks_set_offset (AtomSTCO64 * stco64, guint32 offset) { - guint i; - - for (i = 0; i < atom_array_get_len (&stco64->entries); i++) { - guint64 *value = &atom_array_index (&stco64->entries, i); - - *value += offset; - } + stco64->chunk_offset = offset; } void -atom_moov_chunks_add_offset (AtomMOOV * moov, guint32 offset) +atom_moov_chunks_set_offset (AtomMOOV * moov, guint32 offset) { GList *traks = moov->traks; + if (offset == moov->chunks_offset) + return; /* Nothing to do */ + while (traks) { AtomTRAK *trak = (AtomTRAK *) traks->data; - atom_stco64_chunks_add_offset (&trak->mdia.minf.stbl.stco64, offset); + atom_stco64_chunks_set_offset (&trak->mdia.minf.stbl.stco64, offset); traks = g_list_next (traks); } + + moov->chunks_offset = offset; } void diff --git a/gst/isomp4/atoms.h b/gst/isomp4/atoms.h index 943dfd6ee4..99d45dcf3a 100644 --- a/gst/isomp4/atoms.h +++ b/gst/isomp4/atoms.h @@ -478,7 +478,8 @@ typedef struct _AtomSTSC typedef struct _AtomSTCO64 { AtomFull header; - + /* Global offset to add to entries when serialising */ + guint32 chunk_offset; ATOM_ARRAY (guint64) entries; } AtomSTCO64; @@ -754,6 +755,7 @@ typedef struct _AtomMOOV AtomUDTA udta; gboolean fragmented; + guint32 chunks_offset; } AtomMOOV; typedef struct _AtomWAVE @@ -843,12 +845,13 @@ guint64 atom_moov_copy_data (AtomMOOV *atom, guint8 **buffer, guint64 void atom_moov_update_timescale (AtomMOOV *moov, guint32 timescale); void atom_moov_update_duration (AtomMOOV *moov); void atom_moov_set_fragmented (AtomMOOV *moov, gboolean fragmented); -void atom_moov_chunks_add_offset (AtomMOOV *moov, guint32 offset); +void atom_moov_chunks_set_offset (AtomMOOV *moov, guint32 offset); void atom_moov_add_trak (AtomMOOV *moov, AtomTRAK *trak); +guint atom_moov_get_trak_count (AtomMOOV *moov); guint64 atom_mvhd_copy_data (AtomMVHD * atom, guint8 ** buffer, guint64 * size, guint64 * offset); -void atom_stco64_chunks_add_offset (AtomSTCO64 * stco64, guint32 offset); +void atom_stco64_chunks_set_offset (AtomSTCO64 * stco64, guint32 offset); guint64 atom_trak_copy_data (AtomTRAK * atom, guint8 ** buffer, guint64 * size, guint64 * offset); void atom_stbl_clear (AtomSTBL * stbl); @@ -974,6 +977,7 @@ AtomInfo * build_uuid_xmp_atom (GstBuffer * xmp); /* * Meta tags functions */ +void atom_udta_clear_tags (AtomUDTA *udta); void atom_udta_add_str_tag (AtomUDTA *udta, guint32 fourcc, const gchar *value); void atom_udta_add_uint_tag (AtomUDTA *udta, guint32 fourcc, guint32 flags, guint32 value); diff --git a/gst/isomp4/atomsrecovery.c b/gst/isomp4/atomsrecovery.c index fec76a37d7..83ee372061 100644 --- a/gst/isomp4/atomsrecovery.c +++ b/gst/isomp4/atomsrecovery.c @@ -972,7 +972,7 @@ moov_recov_write_file (MoovRecovFile * moovrf, MdatRecovFile * mdatrf, TrakRecovData *trak = &(moovrf->traks_rd[i]); /* 16 for the mdat header */ gint64 offset = moov_size + ftell (outf) + 16; - atom_stco64_chunks_add_offset (&trak->stbl.stco64, offset); + atom_stco64_chunks_set_offset (&trak->stbl.stco64, offset); } /* write the moov */ diff --git a/gst/isomp4/gstqtmux.c b/gst/isomp4/gstqtmux.c index d20ac9b894..39cc5075fd 100644 --- a/gst/isomp4/gstqtmux.c +++ b/gst/isomp4/gstqtmux.c @@ -2,6 +2,7 @@ * Copyright (C) 2008-2010 Thiago Santos * Copyright (C) 2008 Mark Nauwelaerts * Copyright (C) 2010 Nokia Corporation. All rights reserved. + * Copyright (C) 2014 Jan Schmidt * Contact: Stefan Kost * This library is free software; you can redistribute it and/or @@ -80,6 +81,21 @@ * #GstQTMux:streamable allows foregoing to add index metadata (at the end of * file). * + * When the maximum duration to be recorded can be known in advance, #GstQTMux + * also supports a 'Robust Muxing' mode. In robust muxing mode, space for the + * headers are reserved at the start of muxing, and rewritten at a configurable + * interval, so that the output file is always playable, even if the recording + * is interrupted uncleanly by a crash. Robust muxing mode requires a seekable + * output, such as filesink, because it needs to rewrite the start of the file. + * + * To enable robust muxing mode, set the #GstQTMux::reserved-moov-update-period + * and #GstQTMux::reserved-max-duration property. Also present is the + * #GstQTMux::reserved-bytes-per-sec property, which can be increased if + * for some reason the default is not large enough and the initial reserved + * space for headers is too small. Applications can monitor the + * #GstQTMux::reserved-duration-remaining property to see how close to full + * the reserved space is becoming. + * * * Example pipelines * |[ @@ -146,7 +162,7 @@ GST_DEBUG_CATEGORY_STATIC (gst_qt_mux_debug); * - a 'mfra' box for Fragmented MP4, which is written at the end and * contains a summary of all fragments and seek tables. * - * Currently mp4mux can work in 3 different modes / generate 3 types + * Currently mp4mux can work in 4 different modes / generate 4 types * of output files/streams: * * - Normal mp4: mp4mux will write a little ftyp identifier at the @@ -177,6 +193,19 @@ GST_DEBUG_CATEGORY_STATIC (gst_qt_mux_debug); * out of the temp file at EOS, which can be expensive. Downstream does * not need to be seekable, because of the use of the temp file. * + * - Robust Muxing mode: In this mode, qtmux uses the reserved-max-duration + * and reserved-moov-update-period properties to reserve free space + * at the start of the file and periodically write the MOOV atom out + * to it. That means that killing the muxing at any point still + * results in a playable file, at the cost of wasting some amount of + * free space at the start of file. The approximate recording duration + * has to be known in advance to estimate how much free space to reserve + * for the moov, and the downstream must be seekable. + * If the moov header grows larger than the reserved space, an error + * is generated - so it's better to over-estimate the amount of space + * to reserve. To ensure the file is playable at any point, the moov + * is updated using a 'ping-pong' strategy, so the output is never in + * an invalid state. */ #ifndef GST_REMOVE_DEPRECATED @@ -228,6 +257,10 @@ enum PROP_MOOV_RECOV_FILE, PROP_FRAGMENT_DURATION, PROP_STREAMABLE, + PROP_RESERVED_MAX_DURATION, + PROP_RESERVED_DURATION_REMAINING, + PROP_RESERVED_MOOV_UPDATE_PERIOD, + PROP_RESERVED_BYTES_PER_SEC, #ifndef GST_REMOVE_DEPRECATED PROP_DTS_METHOD, #endif @@ -248,6 +281,9 @@ enum #ifndef GST_REMOVE_DEPRECATED #define DEFAULT_DTS_METHOD DTS_METHOD_REORDER #endif +#define DEFAULT_RESERVED_MAX_DURATION GST_CLOCK_TIME_NONE +#define DEFAULT_RESERVED_MOOV_UPDATE_PERIOD GST_CLOCK_TIME_NONE +#define DEFAULT_RESERVED_BYTES_PER_SEC_PER_TRAK 550 static void gst_qt_mux_finalize (GObject * object); @@ -274,6 +310,9 @@ static GstFlowReturn gst_qt_mux_handle_buffer (GstCollectPads * pads, static GstFlowReturn gst_qt_mux_add_buffer (GstQTMux * qtmux, GstQTPad * pad, GstBuffer * buf); +static GstFlowReturn +gst_qt_mux_robust_recording_rewrite_moov (GstQTMux * qtmux); + static GstElementClass *parent_class = NULL; static void @@ -409,6 +448,34 @@ gst_qt_mux_class_init (GstQTMuxClass * klass) g_object_class_install_property (gobject_class, PROP_STREAMABLE, g_param_spec_boolean ("streamable", "Streamable", streamable_desc, streamable, streamable_flags | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_RESERVED_MAX_DURATION, + g_param_spec_uint64 ("reserved-max-duration", + "Reserved maximum file duration (ns)", + "When set to a value > 0, reserves space for index tables at the " + "beginning of the file.", + 0, G_MAXUINT64, DEFAULT_RESERVED_MAX_DURATION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, + PROP_RESERVED_DURATION_REMAINING, + g_param_spec_uint64 ("reserved-duration-remaining", + "Report the approximate amount of remaining recording space (ns)", + "Reports the approximate amount of remaining moov header space " + "reserved using reserved-max-duration", 0, G_MAXUINT64, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, + PROP_RESERVED_MOOV_UPDATE_PERIOD, + g_param_spec_uint64 ("reserved-moov-update-period", + "Interval at which to update index tables (ns)", + "When used with reserved-max-duration, periodically updates the " + "index tables with information muxed so far.", 0, G_MAXUINT64, + DEFAULT_RESERVED_MOOV_UPDATE_PERIOD, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_RESERVED_BYTES_PER_SEC, + g_param_spec_uint ("reserved-bytes-per-sec", + "Reserved MOOV bytes per second, per track", + "Multiplier for converting reserved-max-duration into bytes of header to reserve, per second, per track", + 0, 10000, DEFAULT_RESERVED_BYTES_PER_SEC_PER_TRAK, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); gstelement_class->request_new_pad = GST_DEBUG_FUNCPTR (gst_qt_mux_request_new_pad); @@ -467,6 +534,7 @@ gst_qt_mux_reset (GstQTMux * qtmux, gboolean alloc) qtmux->state = GST_QT_MUX_STATE_NONE; qtmux->header_size = 0; qtmux->mdat_size = 0; + qtmux->moov_pos = 0; qtmux->mdat_pos = 0; qtmux->longest_chunk = GST_CLOCK_TIME_NONE; qtmux->video_pads = 0; @@ -526,6 +594,11 @@ gst_qt_mux_reset (GstQTMux * qtmux, gboolean alloc) atom_moov_add_trak (qtmux->moov, qtpad->trak); } } + + qtmux->reserved_moov_size = 0; + qtmux->last_moov_update = GST_CLOCK_TIME_NONE; + qtmux->muxed_since_last_update = 0; + qtmux->reserved_duration_remaining = GST_CLOCK_TIME_NONE; } static void @@ -550,6 +623,11 @@ gst_qt_mux_init (GstQTMux * qtmux, GstQTMuxClass * qtmux_klass) /* properties set to default upon construction */ + qtmux->reserved_max_duration = DEFAULT_RESERVED_MAX_DURATION; + qtmux->reserved_moov_update_period = DEFAULT_RESERVED_MOOV_UPDATE_PERIOD; + qtmux->reserved_bytes_per_sec_per_trak = + DEFAULT_RESERVED_BYTES_PER_SEC_PER_TRAK; + /* always need this */ qtmux->context = atoms_context_new (gst_qt_mux_map_format_to_flavor (qtmux_klass->format)); @@ -1208,6 +1286,9 @@ gst_qt_mux_add_metadata_tags (GstQTMux * qtmux, const GstTagList * list, if (!tag_matches) return; + /* Clear existing tags so we don't add them over and over */ + atom_udta_clear_tags (udta); + for (i = 0; tag_matches[i].fourcc; i++) { fourcc = tag_matches[i].fourcc; tag = tag_matches[i].gsttag; @@ -1266,11 +1347,14 @@ gst_qt_mux_add_metadata_tags (GstQTMux * qtmux, const GstTagList * list, static void gst_qt_mux_setup_metadata (GstQTMux * qtmux) { - const GstTagList *tags; + const GstTagList *tags = NULL; GSList *walk; GST_OBJECT_LOCK (qtmux); - tags = gst_tag_setter_get_tag_list (GST_TAG_SETTER (qtmux)); + if (qtmux->tags_changed) { + tags = gst_tag_setter_get_tag_list (GST_TAG_SETTER (qtmux)); + qtmux->tags_changed = FALSE; + } GST_OBJECT_UNLOCK (qtmux); GST_LOG_OBJECT (qtmux, "tags: %" GST_PTR_FORMAT, tags); @@ -1288,7 +1372,7 @@ gst_qt_mux_setup_metadata (GstQTMux * qtmux) gst_qt_mux_add_xmp_tags (qtmux, copy); gst_tag_list_unref (copy); } else { - GST_DEBUG_OBJECT (qtmux, "No tags received"); + GST_DEBUG_OBJECT (qtmux, "No new tags received"); } for (walk = qtmux->sinkpads; walk; walk = g_slist_next (walk)) { @@ -1296,13 +1380,14 @@ gst_qt_mux_setup_metadata (GstQTMux * qtmux) GstQTPad *qpad = (GstQTPad *) cdata; GstPad *pad = qpad->collect.pad; - if (qpad->tags) { + if (qpad->tags_changed && qpad->tags) { GST_DEBUG_OBJECT (pad, "Adding tags"); gst_tag_list_remove_tag (qpad->tags, GST_TAG_CONTAINER_FORMAT); gst_qt_mux_add_metadata_tags (qtmux, qpad->tags, &qpad->trak->udta); + qpad->tags_changed = FALSE; GST_DEBUG_OBJECT (pad, "Tags added"); } else { - GST_DEBUG_OBJECT (pad, "No tags received"); + GST_DEBUG_OBJECT (pad, "No new tags received"); } } } @@ -1460,44 +1545,50 @@ static GstFlowReturn gst_qt_mux_send_mdat_header (GstQTMux * qtmux, guint64 * off, guint64 size, gboolean extended) { - Atom *node_header; GstBuffer *buf; - guint8 *data = NULL; - guint64 offset = 0; + GstMapInfo map; GST_DEBUG_OBJECT (qtmux, "Sending mdat's atom header, " "size %" G_GUINT64_FORMAT, size); - node_header = g_malloc0 (sizeof (Atom)); - node_header->type = FOURCC_mdat; + /* if the qtmux state is EOS, really write the mdat, otherwise + * allow size == 0 for a placeholder atom */ + if (qtmux->state == GST_QT_MUX_STATE_EOS || size > 0) + size += 8; + if (extended) { - /* use extended size */ - node_header->size = 1; - node_header->extended_size = 0; - if (size) - node_header->extended_size = size + 16; + gboolean large_file = (size > MDAT_LARGE_FILE_LIMIT); + /* Always write 16-bytes, but put a free atom first + * if the size is < 4GB. */ + buf = gst_buffer_new_and_alloc (16); + gst_buffer_map (buf, &map, GST_MAP_WRITE); + + if (large_file) { + /* Write extended mdat header and large_size field */ + GST_WRITE_UINT32_BE (map.data, 1); + GST_WRITE_UINT32_LE (map.data + 4, FOURCC_mdat); + GST_WRITE_UINT64_BE (map.data + 8, size + 8); + } else { + /* Write an empty free atom, then standard 32-bit mdat */ + GST_WRITE_UINT32_BE (map.data, 8); + GST_WRITE_UINT32_LE (map.data + 4, FOURCC_free); + GST_WRITE_UINT32_BE (map.data + 8, size); + GST_WRITE_UINT32_LE (map.data + 12, FOURCC_mdat); + } + gst_buffer_unmap (buf, &map); } else { - node_header->size = size + 8; + buf = gst_buffer_new_and_alloc (8); + gst_buffer_map (buf, &map, GST_MAP_WRITE); + + /* Vanilla 32-bit mdat */ + GST_WRITE_UINT32_BE (map.data, size); + GST_WRITE_UINT32_LE (map.data + 4, FOURCC_mdat); + gst_buffer_unmap (buf, &map); } - size = offset = 0; - if (atom_copy_data (node_header, &data, &size, &offset) == 0) - goto serialize_error; - - buf = _gst_buffer_new_take_data (data, offset); - g_free (node_header); - - GST_LOG_OBJECT (qtmux, "Pushing mdat start"); + GST_LOG_OBJECT (qtmux, "Pushing mdat header"); return gst_qt_mux_send_buffer (qtmux, buf, off, FALSE); - /* ERRORS */ -serialize_error: - { - GST_ELEMENT_ERROR (qtmux, STREAM, MUX, (NULL), - ("Failed to serialize mdat")); - g_free (node_header); - return GST_FLOW_ERROR; - } } /* @@ -1508,36 +1599,17 @@ static GstFlowReturn gst_qt_mux_update_mdat_size (GstQTMux * qtmux, guint64 mdat_pos, guint64 mdat_size, guint64 * offset) { - GstBuffer *buf; - gboolean large_file; GstSegment segment; - GstMapInfo map; - large_file = (mdat_size > MDAT_LARGE_FILE_LIMIT); - - if (large_file) - mdat_pos += 8; + /* We must have recorded the mdat position for this to work */ + g_assert (mdat_pos != 0); /* seek and rewrite the header */ gst_segment_init (&segment, GST_FORMAT_BYTES); segment.start = mdat_pos; gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); - if (large_file) { - buf = gst_buffer_new_and_alloc (sizeof (guint64)); - gst_buffer_map (buf, &map, GST_MAP_WRITE); - GST_WRITE_UINT64_BE (map.data, mdat_size + 16); - } else { - buf = gst_buffer_new_and_alloc (16); - gst_buffer_map (buf, &map, GST_MAP_WRITE); - GST_WRITE_UINT32_BE (map.data, 8); - GST_WRITE_UINT32_LE (map.data + 4, FOURCC_free); - GST_WRITE_UINT32_BE (map.data + 8, mdat_size + 8); - GST_WRITE_UINT32_LE (map.data + 12, FOURCC_mdat); - } - gst_buffer_unmap (buf, &map); - - return gst_qt_mux_send_buffer (qtmux, buf, offset, FALSE); + return gst_qt_mux_send_mdat_header (qtmux, offset, mdat_size, TRUE); } static GstFlowReturn @@ -1644,15 +1716,79 @@ gst_qt_mux_set_header_on_caps (GstQTMux * mux, GstBuffer * buf) gst_caps_unref (caps); } -static void -gst_qt_mux_configure_moov (GstQTMux * qtmux, guint32 * _timescale) +/* + * Write out a free space atom. The offset is adjusted by the full + * size, but a smaller buffer is sent + */ +static GstFlowReturn +gst_qt_mux_send_free_atom (GstQTMux * qtmux, guint64 * off, guint32 size) { - gboolean fragmented; + Atom *node_header; + GstBuffer *buf; + guint8 *data = NULL; + guint64 offset = 0, bsize = 0; + GstFlowReturn ret; + + GST_DEBUG_OBJECT (qtmux, "Sending free atom header of size %u", size); + + /* We can't make a free space atom smaller than the header */ + if (size < 8) + goto too_small; + + node_header = g_malloc0 (sizeof (Atom)); + node_header->type = FOURCC_free; + node_header->size = size; + + bsize = offset = 0; + if (atom_copy_data (node_header, &data, &bsize, &offset) == 0) + goto serialize_error; + + buf = _gst_buffer_new_take_data (data, offset); + g_free (node_header); + + GST_LOG_OBJECT (qtmux, "Pushing free atom"); + ret = gst_qt_mux_send_buffer (qtmux, buf, off, FALSE); + + if (off) { + GstSegment segment; + + *off += size - 8; + + /* Make sure downstream position ends up at the end of this free box */ + gst_segment_init (&segment, GST_FORMAT_BYTES); + segment.start = *off; + gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); + } + + return ret; + + /* ERRORS */ +too_small: + { + GST_ELEMENT_ERROR (qtmux, STREAM, MUX, (NULL), + ("Not enough free reserved space")); + return GST_FLOW_ERROR; + } +serialize_error: + { + GST_ELEMENT_ERROR (qtmux, STREAM, MUX, (NULL), + ("Failed to serialize mdat")); + g_free (node_header); + return GST_FLOW_ERROR; + } +} + +static void +gst_qt_mux_configure_moov (GstQTMux * qtmux) +{ + gboolean fragmented = FALSE; guint32 timescale; GST_OBJECT_LOCK (qtmux); timescale = qtmux->timescale; - fragmented = qtmux->fragment_sequence > 0; + if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED || + qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE) + fragmented = TRUE; GST_OBJECT_UNLOCK (qtmux); /* inform lower layers of our property wishes, and determine duration. @@ -1664,13 +1800,11 @@ gst_qt_mux_configure_moov (GstQTMux * qtmux, guint32 * _timescale) atom_moov_set_fragmented (qtmux->moov, fragmented); atom_moov_update_duration (qtmux->moov); - - if (_timescale) - *_timescale = timescale; } static GstFlowReturn -gst_qt_mux_send_moov (GstQTMux * qtmux, guint64 * _offset, gboolean mind_fast) +gst_qt_mux_send_moov (GstQTMux * qtmux, guint64 * _offset, + guint64 padded_moov_size, gboolean mind_fast) { guint64 offset = 0, size = 0; guint8 *data; @@ -1683,14 +1817,32 @@ gst_qt_mux_send_moov (GstQTMux * qtmux, guint64 * _offset, gboolean mind_fast) GST_LOG_OBJECT (qtmux, "Copying movie header into buffer"); if (!atom_moov_copy_data (qtmux->moov, &data, &size, &offset)) goto serialize_error; + qtmux->last_moov_size = offset; + /* Check we have enough reserved space for this and a Free atom */ + if (padded_moov_size > 0 && offset + 8 > padded_moov_size) + goto too_small_reserved; buf = _gst_buffer_new_take_data (data, offset); GST_DEBUG_OBJECT (qtmux, "Pushing moov atoms"); gst_qt_mux_set_header_on_caps (qtmux, buf); ret = gst_qt_mux_send_buffer (qtmux, buf, _offset, mind_fast); - return ret; + /* Write out a free atom if needed */ + if (ret == GST_FLOW_OK && offset < padded_moov_size) { + GST_LOG_OBJECT (qtmux, "Writing out free atom of size %u", + (guint32) (padded_moov_size - offset)); + ret = gst_qt_mux_send_free_atom (qtmux, _offset, padded_moov_size - offset); + } + return ret; +too_small_reserved: + { + GST_ELEMENT_ERROR (qtmux, STREAM, MUX, + ("Not enough free reserved header space"), + ("Needed %" G_GUINT64_FORMAT " bytes, reserved %" G_GUINT64_FORMAT, + offset, padded_moov_size)); + return GST_FLOW_ERROR; + } serialize_error: { g_free (data); @@ -1819,9 +1971,16 @@ gst_qt_mux_start_file (GstQTMux * qtmux) GstCaps *caps; GstSegment segment; gchar s_id[32]; + GstClockTime reserved_max_duration; + guint reserved_bytes_per_sec_per_trak; GST_DEBUG_OBJECT (qtmux, "starting file"); + GST_OBJECT_LOCK (qtmux); + reserved_max_duration = qtmux->reserved_max_duration; + reserved_bytes_per_sec_per_trak = qtmux->reserved_bytes_per_sec_per_trak; + GST_OBJECT_UNLOCK (qtmux); + /* stream-start (FIXME: create id based on input ids) */ g_snprintf (s_id, sizeof (s_id), "qtmux-%08x", g_random_int ()); gst_pad_push_event (qtmux->srcpad, gst_event_new_stream_start (s_id)); @@ -1849,10 +2008,13 @@ gst_qt_mux_start_file (GstQTMux * qtmux) qtmux->mux_mode = GST_QT_MUX_MODE_FRAGMENTED; } else if (qtmux->fast_start) { qtmux->mux_mode = GST_QT_MUX_MODE_FAST_START; + } else if (reserved_max_duration != GST_CLOCK_TIME_NONE) { + qtmux->mux_mode = GST_QT_MUX_MODE_ROBUST_RECORDING; } switch (qtmux->mux_mode) { case GST_QT_MUX_MODE_MOOV_AT_END: + case GST_QT_MUX_MODE_ROBUST_RECORDING: /* We have to be able to seek to rewrite the mdat header, or any * moov atom we write will not be visible in the file, because an * MDAT with 0 as the size covers the rest of the file. A file @@ -1887,6 +2049,11 @@ gst_qt_mux_start_file (GstQTMux * qtmux) if (qtmux->moov_recov_file_path) { gst_qt_mux_prepare_moov_recovery (qtmux); } + + /* Make sure the first time we update the moov, we'll + * include any tagsetter tags */ + qtmux->tags_changed = TRUE; + GST_OBJECT_UNLOCK (qtmux); /* @@ -1901,7 +2068,99 @@ gst_qt_mux_start_file (GstQTMux * qtmux) if (ret != GST_FLOW_OK) break; - /* store the mdat position for rewriting later ... */ + /* Store this as the mdat offset for later updating + * when we write the moov */ + qtmux->mdat_pos = qtmux->header_size; + /* extended atom in case we go over 4GB while writing and need + * the full 64-bit atom */ + ret = gst_qt_mux_send_mdat_header (qtmux, &qtmux->header_size, 0, TRUE); + break; + case GST_QT_MUX_MODE_ROBUST_RECORDING: + + ret = gst_qt_mux_prepare_and_send_ftyp (qtmux); + if (ret != GST_FLOW_OK) + break; + + /* Pad ftyp out to an 8-byte boundary before starting the moov + * ping pong region. It should be well less than 1 disk sector, + * unless there's a bajillion compatible types listed, + * but let's be sure the free atom doesn't cross a sector + * boundary anyway */ + if (qtmux->header_size % 8) { + /* Extra 8 bytes for the padding free atom header */ + guint padding = (guint) (16 - (qtmux->header_size % 8)); + GST_LOG_OBJECT (qtmux, "Rounding ftyp by %u bytes", padding); + ret = gst_qt_mux_send_free_atom (qtmux, &qtmux->header_size, padding); + if (ret != GST_FLOW_OK) + return ret; + } + + /* Store this as the moov offset for later updating. + * We record mdat position below */ + qtmux->moov_pos = qtmux->header_size; + + /* Set up the initial 'ping' state of the ping-pong buffers */ + qtmux->reserved_moov_first_active = TRUE; + + gst_qt_mux_configure_moov (qtmux); + gst_qt_mux_setup_metadata (qtmux); + /* Empty free atom to begin, starting on an 8-byte boundary */ + ret = gst_qt_mux_send_free_atom (qtmux, &qtmux->header_size, 8); + if (ret != GST_FLOW_OK) + return ret; + /* Moov header, not padded yet */ + ret = gst_qt_mux_send_moov (qtmux, &qtmux->header_size, 0, FALSE); + if (ret != GST_FLOW_OK) + return ret; + /* The moov we just sent contains the 'base' size of the moov, before + * we put in any time-dependent per-trak data. Use that to make + * a good estimate of how much extra to reserve */ + /* Calculate how much space to reserve for our MOOV atom. + * We actually reserve twice that, for ping-pong buffers */ + qtmux->base_moov_size = qtmux->last_moov_size; + GST_LOG_OBJECT (qtmux, "Base moov size is %u before any indexes", + qtmux->base_moov_size); + qtmux->reserved_moov_size = qtmux->base_moov_size + + gst_util_uint64_scale (reserved_max_duration, + reserved_bytes_per_sec_per_trak * + atom_moov_get_trak_count (qtmux->moov), GST_SECOND); + + /* Need space for at least 4 atom headers. More really, but + * this as an absolute minimum */ + if (qtmux->reserved_moov_size < 4 * 8) + goto reserved_moov_too_small; + + GST_DEBUG_OBJECT (qtmux, "reserving header area of size %u", + 2 * qtmux->reserved_moov_size + 16); + + GST_OBJECT_LOCK (qtmux); + qtmux->reserved_duration_remaining = + gst_util_uint64_scale (qtmux->reserved_moov_size - + qtmux->base_moov_size, GST_SECOND, + reserved_bytes_per_sec_per_trak * + atom_moov_get_trak_count (qtmux->moov)); + GST_OBJECT_UNLOCK (qtmux); + + /* Now that we know how much reserved space is targetted, + * output a free atom to fill the extra reserved */ + ret = gst_qt_mux_send_free_atom (qtmux, &qtmux->header_size, + qtmux->reserved_moov_size - qtmux->base_moov_size); + if (ret != GST_FLOW_OK) + return ret; + + /* Then a free atom containing 'pong' buffer, with an + * extra 8 bytes to account for the free atom header itself */ + ret = gst_qt_mux_send_free_atom (qtmux, &qtmux->header_size, + qtmux->reserved_moov_size + 8); + if (ret != GST_FLOW_OK) + return ret; + + /* extra atoms go after the free/moov(s), before the mdat */ + ret = + gst_qt_mux_send_extra_atoms (qtmux, TRUE, &qtmux->header_size, FALSE); + if (ret != GST_FLOW_OK) + return ret; + qtmux->mdat_pos = qtmux->header_size; /* extended atom in case we go over 4GB while writing and need * the full 64-bit atom */ @@ -1921,16 +2180,18 @@ gst_qt_mux_start_file (GstQTMux * qtmux) ret = gst_qt_mux_prepare_and_send_ftyp (qtmux); if (ret != GST_FLOW_OK) break; - /* well, it's moov pos if fragmented ... */ - qtmux->mdat_pos = qtmux->header_size; + /* store the moov pos so we can update the duration later + * in non-streamable mode */ + qtmux->moov_pos = qtmux->header_size; + GST_DEBUG_OBJECT (qtmux, "fragment duration %d ms, writing headers", qtmux->fragment_duration); /* also used as snapshot marker to indicate fragmented file */ qtmux->fragment_sequence = 1; /* prepare moov and/or tags */ - gst_qt_mux_configure_moov (qtmux, NULL); + gst_qt_mux_configure_moov (qtmux); gst_qt_mux_setup_metadata (qtmux); - ret = gst_qt_mux_send_moov (qtmux, &qtmux->header_size, FALSE); + ret = gst_qt_mux_send_moov (qtmux, &qtmux->header_size, 0, FALSE); if (ret != GST_FLOW_OK) return ret; /* extra atoms */ @@ -1952,7 +2213,12 @@ invalid_isml: ("Cannot create an ISML file with 0 fragment duration"), (NULL)); return GST_FLOW_ERROR; } - +reserved_moov_too_small: + { + GST_ELEMENT_ERROR (qtmux, STREAM, MUX, + ("Not enough reserved space for creating headers"), (NULL)); + return GST_FLOW_ERROR; + } open_failed: { GST_ELEMENT_ERROR (qtmux, RESOURCE, OPEN_READ_WRITE, @@ -1964,22 +2230,11 @@ open_failed: } static GstFlowReturn -gst_qt_mux_stop_file (GstQTMux * qtmux) +gst_qt_mux_send_last_buffers (GstQTMux * qtmux) { - gboolean ret = GST_FLOW_OK; - guint64 offset = 0, size = 0; + GstFlowReturn ret = GST_FLOW_OK; GSList *walk; - gboolean large_file; - guint32 timescale; - GstClockTime first_ts = GST_CLOCK_TIME_NONE; - /* for setting some subtitles fields */ - guint max_width = 0; - guint max_height = 0; - - GST_DEBUG_OBJECT (qtmux, "Updating remaining values and sending last data"); - - /* pushing last buffers for each pad */ for (walk = qtmux->collect->data; walk; walk = g_slist_next (walk)) { GstCollectData *cdata = (GstCollectData *) walk->data; GstQTPad *qtpad = (GstQTPad *) cdata; @@ -2001,19 +2256,43 @@ gst_qt_mux_stop_file (GstQTMux * qtmux) "flow return: %s", GST_PAD_NAME (qtpad->collect.pad), gst_flow_get_name (ret)); } + } - /* having flushed above, can check for buffers now */ - if (!GST_CLOCK_TIME_IS_VALID (qtpad->first_ts)) { - GST_DEBUG_OBJECT (qtmux, "Pad %s has no buffers", + return ret; +} + +static void +gst_qt_mux_update_global_statistics (GstQTMux * qtmux) +{ + GSList *walk; + + /* for setting some subtitles fields */ + guint max_width = 0; + guint max_height = 0; + + qtmux->first_ts = qtmux->last_dts = GST_CLOCK_TIME_NONE; + + for (walk = qtmux->collect->data; walk; walk = g_slist_next (walk)) { + GstCollectData *cdata = (GstCollectData *) walk->data; + GstQTPad *qtpad = (GstQTPad *) cdata; + + if (!qtpad->fourcc) { + GST_DEBUG_OBJECT (qtmux, "Pad %s has never had buffers", GST_PAD_NAME (qtpad->collect.pad)); continue; } - /* determine max stream duration */ - if (!GST_CLOCK_TIME_IS_VALID (first_ts) || - (GST_CLOCK_TIME_IS_VALID (qtpad->first_ts) && - qtpad->last_dts > first_ts)) { - first_ts = qtpad->last_dts; + /* having flushed above, can check for buffers now */ + if (GST_CLOCK_TIME_IS_VALID (qtpad->first_ts)) { + /* determine max stream duration */ + if (!GST_CLOCK_TIME_IS_VALID (qtmux->last_dts) + || qtpad->last_dts > qtmux->last_dts) { + qtmux->last_dts = qtpad->last_dts; + } + if (!GST_CLOCK_TIME_IS_VALID (qtmux->first_ts) + || qtpad->first_ts < qtmux->first_ts) { + qtmux->first_ts = qtpad->first_ts; + } } /* subtitles need to know the video width/height, @@ -2053,67 +2332,20 @@ gst_qt_mux_stop_file (GstQTMux * qtmux) atom_trak_tx3g_update_dimension (qtpad->trak, max_width, max_height); } } +} - switch (qtmux->mux_mode) { - case GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE: - { - /* Streamable mode; no need to write duration or MFRA */ - GST_DEBUG_OBJECT (qtmux, "streamable file; nothing to stop"); - return GST_FLOW_OK; - } - case GST_QT_MUX_MODE_FRAGMENTED: - { - GstSegment segment; - guint8 *data = NULL; - GstBuffer *buf; - - size = offset = 0; - GST_DEBUG_OBJECT (qtmux, "adding mfra"); - if (!atom_mfra_copy_data (qtmux->mfra, &data, &size, &offset)) - goto serialize_error; - buf = _gst_buffer_new_take_data (data, offset); - ret = gst_qt_mux_send_buffer (qtmux, buf, NULL, FALSE); - if (ret != GST_FLOW_OK) - return ret; - - timescale = qtmux->timescale; - /* only mvex duration is updated, - * mvhd should be consistent with empty moov - * (but TODO maybe some clients do not handle that well ?) */ - qtmux->moov->mvex.mehd.fragment_duration = - gst_util_uint64_scale (first_ts, timescale, GST_SECOND); - GST_DEBUG_OBJECT (qtmux, "rewriting moov with mvex duration %" - GST_TIME_FORMAT, GST_TIME_ARGS (first_ts)); - /* seek and rewrite the header */ - gst_segment_init (&segment, GST_FORMAT_BYTES); - segment.start = qtmux->mdat_pos; - gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); - /* no need to seek back */ - return gst_qt_mux_send_moov (qtmux, NULL, FALSE); - } - default: - break; - } - - /* Moov-at-end or fast-start mode from here down */ - gst_qt_mux_configure_moov (qtmux, ×cale); - /* check for late streams. First, find the earliest start time */ - first_ts = GST_CLOCK_TIME_NONE; - for (walk = qtmux->collect->data; walk; walk = g_slist_next (walk)) { - GstCollectData *cdata = (GstCollectData *) walk->data; - GstQTPad *qtpad = (GstQTPad *) cdata; - - if (!GST_CLOCK_TIME_IS_VALID (first_ts) || - (GST_CLOCK_TIME_IS_VALID (qtpad->first_ts) && - qtpad->first_ts < first_ts)) { - first_ts = qtpad->first_ts; - } - } +/* Called after gst_qt_mux_update_global_statistics() updates the + * first_ts tracking, to create/set edit lists for delayed streams */ +static void +gst_qt_mux_update_edit_lists (GstQTMux * qtmux) +{ + GSList *walk; GST_DEBUG_OBJECT (qtmux, "Media first ts selected: %" GST_TIME_FORMAT, - GST_TIME_ARGS (first_ts)); + GST_TIME_ARGS (qtmux->first_ts)); /* add/update EDTSs for late streams. configure_moov will have - * set the trak durations above by summing the sample tables */ + * set the trak durations above by summing the sample tables, + * here we extend that if needing to insert an empty segment */ for (walk = qtmux->collect->data; walk; walk = g_slist_next (walk)) { GstCollectData *cdata = (GstCollectData *) walk->data; GstQTPad *qtpad = (GstQTPad *) cdata; @@ -2121,14 +2353,14 @@ gst_qt_mux_stop_file (GstQTMux * qtmux) guint32 duration; if (GST_CLOCK_TIME_IS_VALID (qtpad->first_ts) - && qtpad->first_ts > first_ts) { - GST_DEBUG_OBJECT (qtmux, "Pad %s is a late stream by %" GST_TIME_FORMAT, - GST_PAD_NAME (qtpad->collect.pad), - GST_TIME_ARGS (qtpad->first_ts - first_ts)); + && qtpad->first_ts > qtmux->first_ts) { lateness = - gst_util_uint64_scale_round (qtpad->first_ts - first_ts, timescale, - GST_SECOND); + gst_util_uint64_scale_round (qtpad->first_ts - qtmux->first_ts, + qtmux->timescale, GST_SECOND); duration = qtpad->trak->tkhd.duration; + + GST_DEBUG_OBJECT (qtmux, "Pad %s is a late stream by %" GST_TIME_FORMAT, + GST_PAD_NAME (qtpad->collect.pad), GST_TIME_ARGS (lateness)); atom_trak_set_elst_entry (qtpad->trak, 0, lateness, (guint32) - 1, (guint32) (1 * 65536.0)); atom_trak_set_elst_entry (qtpad->trak, 1, duration, 0, @@ -2146,45 +2378,119 @@ gst_qt_mux_stop_file (GstQTMux * qtmux) } } } +} + +static GstFlowReturn +gst_qt_mux_stop_file (GstQTMux * qtmux, gboolean at_eos) +{ + gboolean ret = GST_FLOW_OK; + guint64 offset = 0, size = 0; + gboolean large_file; + + GST_DEBUG_OBJECT (qtmux, "Updating remaining values and sending last data"); + + /* pushing last buffers for each pad */ + if (at_eos && (ret = gst_qt_mux_send_last_buffers (qtmux)) != GST_FLOW_OK) + return ret; + + if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE) { + /* Streamable mode; no need to write duration or MFRA */ + GST_DEBUG_OBJECT (qtmux, "streamable file; nothing to stop"); + return GST_FLOW_OK; + } + + gst_qt_mux_update_global_statistics (qtmux); + + switch (qtmux->mux_mode) { + case GST_QT_MUX_MODE_FRAGMENTED:{ + GstSegment segment; + guint8 *data = NULL; + GstBuffer *buf; + + size = offset = 0; + GST_DEBUG_OBJECT (qtmux, "adding mfra"); + if (!atom_mfra_copy_data (qtmux->mfra, &data, &size, &offset)) + goto serialize_error; + buf = _gst_buffer_new_take_data (data, offset); + ret = gst_qt_mux_send_buffer (qtmux, buf, NULL, FALSE); + if (ret != GST_FLOW_OK) + return ret; + + /* only mvex duration is updated, + * mvhd should be consistent with empty moov + * (but TODO maybe some clients do not handle that well ?) */ + qtmux->moov->mvex.mehd.fragment_duration = + gst_util_uint64_scale (qtmux->last_dts, qtmux->timescale, GST_SECOND); + GST_DEBUG_OBJECT (qtmux, "rewriting moov with mvex duration %" + GST_TIME_FORMAT, GST_TIME_ARGS (qtmux->last_dts)); + /* seek and rewrite the header */ + gst_segment_init (&segment, GST_FORMAT_BYTES); + segment.start = qtmux->moov_pos; + gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); + /* no need to seek back */ + return gst_qt_mux_send_moov (qtmux, NULL, 0, FALSE); + } + case GST_QT_MUX_MODE_ROBUST_RECORDING:{ + ret = gst_qt_mux_robust_recording_rewrite_moov (qtmux); + if (G_UNLIKELY (ret != GST_FLOW_OK)) + return ret; + /* Finalise by writing the final size into the mdat. Up until now + * it's been 0, which means 'rest of the file' + * No need to seek back after this, we won't write any more */ + return gst_qt_mux_update_mdat_size (qtmux, qtmux->mdat_pos, + qtmux->mdat_size, NULL); + } + default: + break; + } + + /* Moov-at-end or fast-start mode from here down */ + gst_qt_mux_configure_moov (qtmux); + + gst_qt_mux_update_edit_lists (qtmux); /* tags into file metadata */ gst_qt_mux_setup_metadata (qtmux); + large_file = (qtmux->mdat_size > MDAT_LARGE_FILE_LIMIT); - /* if faststart, update the offset of the atoms in the movie with the offset - * that the movie headers before mdat will cause. - * Also, send the ftyp */ - if (qtmux->mux_mode == GST_QT_MUX_MODE_FAST_START) { - GstFlowReturn flow_ret; - offset = size = 0; + switch (qtmux->mux_mode) { + case GST_QT_MUX_MODE_FAST_START:{ + /* if faststart, update the offset of the atoms in the movie with the offset + * that the movie headers before mdat will cause. + * Also, send the ftyp */ + offset = size = 0; - flow_ret = gst_qt_mux_prepare_and_send_ftyp (qtmux); - if (flow_ret != GST_FLOW_OK) { - goto ftyp_error; + ret = gst_qt_mux_prepare_and_send_ftyp (qtmux); + if (ret != GST_FLOW_OK) { + goto ftyp_error; + } + /* copy into NULL to obtain size */ + if (!atom_moov_copy_data (qtmux->moov, NULL, &size, &offset)) + goto serialize_error; + GST_DEBUG_OBJECT (qtmux, "calculated moov atom size %" G_GUINT64_FORMAT, + offset); + offset += qtmux->header_size + (large_file ? 16 : 8); + + /* sum up with the extra atoms size */ + ret = gst_qt_mux_send_extra_atoms (qtmux, FALSE, &offset, FALSE); + if (ret != GST_FLOW_OK) + return ret; + break; } - /* copy into NULL to obtain size */ - if (!atom_moov_copy_data (qtmux->moov, NULL, &size, &offset)) - goto serialize_error; - GST_DEBUG_OBJECT (qtmux, "calculated moov atom size %" G_GUINT64_FORMAT, - offset); - offset += qtmux->header_size + (large_file ? 16 : 8); - - /* sum up with the extra atoms size */ - ret = gst_qt_mux_send_extra_atoms (qtmux, FALSE, &offset, FALSE); - if (ret != GST_FLOW_OK) - return ret; - } else { - offset = qtmux->header_size; + default: + offset = qtmux->header_size; + break; } /* Now that we know the size of moov + extra atoms, we can adjust * the chunk offsets stored into the moov */ - atom_moov_chunks_add_offset (qtmux->moov, offset); + atom_moov_chunks_set_offset (qtmux->moov, offset); /* write out moov and extra atoms */ /* note: as of this point, we no longer care about tracking written data size, * since there is no more use for it anyway */ - ret = gst_qt_mux_send_moov (qtmux, NULL, FALSE); + ret = gst_qt_mux_send_moov (qtmux, NULL, 0, FALSE); if (ret != GST_FLOW_OK) return ret; @@ -2333,6 +2639,172 @@ init: return ret; } +/* Here's the clever bit of robust recording: Updating the moov + * header is done using a ping-pong scheme inside 2 blocks of size + * 'reserved_moov_size' at the start of the file, in such a way that the + * file on-disk is always valid if interrupted. + * Inside the reserved space, we have 2 pairs of free + moov atoms + * (in that order), free-A + moov-A @ offset 0 and free-B + moov-B at + * at offset "reserved_moov_size". + * + * 1. Free-A has 0 size payload, moov-A immediately after is + * active/current, and is padded with an internal Free atom to + * end at reserved_space/2. Free-B is at reserved_space/2, sized + * to cover the remaining free space (including moov-B). + * 2. We write moov-B (which is invisible inside free-B), and pad it to + * end at the end of free space. Then, we update free-A to size + * reserved_space/2 + sizeof(free-B), which hides moov-A and the + * free-B header, and makes moov-B active. + * 3. Rewrite moov-A inside free-A, with padding out to free-B. + * Change the size of free-A to make moov-A active again. + * 4. Rinse and repeat. + * + */ +static GstFlowReturn +gst_qt_mux_robust_recording_rewrite_moov (GstQTMux * qtmux) +{ + GstSegment segment; + GstFlowReturn ret; + guint64 freeA_offset; + guint32 new_freeA_size; + guint64 new_moov_offset; + + /* Update moov info, then seek and rewrite the MOOV atom */ + gst_qt_mux_update_global_statistics (qtmux); + gst_qt_mux_configure_moov (qtmux); + + gst_qt_mux_update_edit_lists (qtmux); + + /* tags into file metadata */ + gst_qt_mux_setup_metadata (qtmux); + + /* chunks position is set relative to the first byte of the + * MDAT atom payload. Set the overall offset into the file */ + atom_moov_chunks_set_offset (qtmux->moov, qtmux->header_size); + + /* Calculate which moov to rewrite. qtmux->moov_pos points to + * the start of the free-A header */ + freeA_offset = qtmux->moov_pos; + if (qtmux->reserved_moov_first_active) { + GST_DEBUG_OBJECT (qtmux, "Updating pong moov header"); + /* After this, freeA will include itself, moovA, plus the freeB + * header */ + new_freeA_size = qtmux->reserved_moov_size + 16; + } else { + GST_DEBUG_OBJECT (qtmux, "Updating ping moov header"); + new_freeA_size = 8; + } + /* the moov we update is after free-A, calculate its offset */ + new_moov_offset = freeA_offset + new_freeA_size; + + /* Swap ping-pong cadence marker */ + qtmux->reserved_moov_first_active = !qtmux->reserved_moov_first_active; + + /* seek and rewrite the MOOV atom */ + gst_segment_init (&segment, GST_FORMAT_BYTES); + segment.start = new_moov_offset; + gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); + + ret = gst_qt_mux_send_moov (qtmux, NULL, qtmux->reserved_moov_size, FALSE); + if (ret != GST_FLOW_OK) + return ret; + + /* Update the estimated recording space remaining, based on amount used so + * far and duration muxed so far */ + if (qtmux->last_moov_size > qtmux->base_moov_size && qtmux->last_dts > 0) { + GstClockTime remain; + GstClockTime time_muxed = qtmux->last_dts; + + remain = + gst_util_uint64_scale (qtmux->reserved_moov_size - + qtmux->last_moov_size, time_muxed, + qtmux->last_moov_size - qtmux->base_moov_size); + /* Always under-estimate slightly, so users + * have time to stop muxing before we run out */ + if (remain < GST_SECOND / 2) + remain = 0; + else + remain -= GST_SECOND / 2; + + GST_INFO_OBJECT (qtmux, + "Reserved %u header bytes. Used %u in %" GST_TIME_FORMAT + ". Remaining now %u or approx %" G_GUINT64_FORMAT " ns\n", + qtmux->reserved_moov_size, qtmux->last_moov_size, + GST_TIME_ARGS (qtmux->last_dts), + qtmux->reserved_moov_size - qtmux->last_moov_size, remain); + + GST_OBJECT_LOCK (qtmux); + qtmux->reserved_duration_remaining = remain; + qtmux->muxed_since_last_update = 0; + GST_DEBUG_OBJECT (qtmux, "reserved remaining duration now %" + G_GUINT64_FORMAT, qtmux->reserved_duration_remaining); + GST_OBJECT_UNLOCK (qtmux); + } + + + /* Now update the moov-A size. Don't pass offset, since we don't need + * send_free_atom() to seek for us - all our callers seek back to + * where they need after this, or they don't need it */ + gst_segment_init (&segment, GST_FORMAT_BYTES); + segment.start = freeA_offset; + gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); + + ret = gst_qt_mux_send_free_atom (qtmux, NULL, new_freeA_size); + + return ret; +} + +static GstFlowReturn +gst_qt_mux_robust_recording_update (GstQTMux * qtmux, GstClockTime position) +{ + GstSegment segment; + GstFlowReturn flow_ret; + + guint64 mdat_offset = qtmux->mdat_pos + 16 + qtmux->mdat_size; + + GST_OBJECT_LOCK (qtmux); + if (qtmux->reserved_moov_update_period == GST_CLOCK_TIME_NONE) { + GST_OBJECT_UNLOCK (qtmux); + return GST_FLOW_OK; + } + + /* Update if position is > the threshold or there's been no update yet */ + if (qtmux->last_moov_update != GST_CLOCK_TIME_NONE && + (position <= qtmux->last_moov_update || + (position - qtmux->last_moov_update) < + qtmux->reserved_moov_update_period)) { + /* Update the offset of how much we've muxed, so the + * report of remaining space keeps counting down */ + if (position > qtmux->last_moov_update && + position - qtmux->last_moov_update > qtmux->muxed_since_last_update) { + GST_LOG_OBJECT (qtmux, + "Muxed time %" G_GUINT64_FORMAT " since last moov update", + qtmux->muxed_since_last_update); + qtmux->muxed_since_last_update = position - qtmux->last_moov_update; + } + GST_OBJECT_UNLOCK (qtmux); + return GST_FLOW_OK; /* No update needed yet */ + } + + qtmux->last_moov_update = position; + GST_OBJECT_UNLOCK (qtmux); + + GST_DEBUG_OBJECT (qtmux, "Update moov atom, position %" GST_TIME_FORMAT + " mdat starts @ %" G_GUINT64_FORMAT " we were a %" G_GUINT64_FORMAT, + GST_TIME_ARGS (position), qtmux->mdat_pos, mdat_offset); + + flow_ret = gst_qt_mux_robust_recording_rewrite_moov (qtmux); + if (G_UNLIKELY (flow_ret != GST_FLOW_OK)) + return flow_ret; + + /* Seek back to previous position */ + gst_segment_init (&segment, GST_FORMAT_BYTES); + segment.start = mdat_offset; + gst_pad_push_event (qtmux->srcpad, gst_event_new_segment (&segment)); + + return flow_ret; +} + static GstFlowReturn gst_qt_mux_register_and_push_sample (GstQTMux * qtmux, GstQTPad * pad, GstBuffer * buffer, gboolean is_last_buffer, guint nsamples, @@ -2356,10 +2828,14 @@ gst_qt_mux_register_and_push_sample (GstQTMux * qtmux, GstQTPad * pad, switch (qtmux->mux_mode) { case GST_QT_MUX_MODE_MOOV_AT_END: case GST_QT_MUX_MODE_FAST_START: - + case GST_QT_MUX_MODE_ROBUST_RECORDING: atom_trak_add_samples (pad->trak, nsamples, (gint32) scaled_duration, sample_size, chunk_offset, sync, pts_offset); ret = gst_qt_mux_send_buffer (qtmux, buffer, &qtmux->mdat_size, TRUE); + /* Check if it's time to re-write the headers in robust-recording mode */ + if (ret == GST_FLOW_OK + && qtmux->mux_mode == GST_QT_MUX_MODE_ROBUST_RECORDING) + ret = gst_qt_mux_robust_recording_update (qtmux, pad->total_duration); break; case GST_QT_MUX_MODE_FRAGMENTED: case GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE: @@ -2681,8 +3157,8 @@ gst_qt_mux_handle_buffer (GstCollectPads * pads, GstCollectData * cdata, if (G_UNLIKELY (qtmux->state == GST_QT_MUX_STATE_STARTED)) { if ((ret = gst_qt_mux_start_file (qtmux)) != GST_FLOW_OK) return ret; - else - qtmux->state = GST_QT_MUX_STATE_DATA; + + qtmux->state = GST_QT_MUX_STATE_DATA; } if (G_UNLIKELY (qtmux->state == GST_QT_MUX_STATE_EOS)) @@ -2698,7 +3174,7 @@ gst_qt_mux_handle_buffer (GstCollectPads * pads, GstCollectData * cdata, GST_PAD_NAME (best_pad->collect.pad), GST_TIME_ARGS (best_time)); ret = gst_qt_mux_add_buffer (qtmux, best_pad, buf); } else { - ret = gst_qt_mux_stop_file (qtmux); + ret = gst_qt_mux_stop_file (qtmux, TRUE); if (ret == GST_FLOW_OK) { GST_DEBUG_OBJECT (qtmux, "Pushing eos"); gst_pad_push_event (qtmux->srcpad, gst_event_new_eos ()); @@ -3511,10 +3987,12 @@ gst_qt_mux_sink_event (GstCollectPads * pads, GstCollectData * data, if (gst_tag_list_get_scope (list) == GST_TAG_SCOPE_GLOBAL) { gst_tag_setter_merge_tags (setter, list, mode); + qtmux->tags_changed = TRUE; } else { if (!collect_pad->tags) collect_pad->tags = gst_tag_list_new_empty (); gst_tag_list_insert (collect_pad->tags, list, mode); + collect_pad->tags_changed = TRUE; } GST_OBJECT_UNLOCK (qtmux); @@ -3703,6 +4181,34 @@ gst_qt_mux_get_property (GObject * object, case PROP_STREAMABLE: g_value_set_boolean (value, qtmux->streamable); break; + case PROP_RESERVED_MAX_DURATION: + g_value_set_uint64 (value, qtmux->reserved_max_duration); + break; + case PROP_RESERVED_DURATION_REMAINING: + if (qtmux->reserved_duration_remaining == GST_CLOCK_TIME_NONE) + g_value_set_uint64 (value, qtmux->reserved_max_duration); + else { + GstClockTime remaining = qtmux->reserved_duration_remaining; + + /* Report the remaining space as the calculated remaining, minus + * however much we've muxed since the last update */ + if (remaining > qtmux->muxed_since_last_update) + remaining -= qtmux->muxed_since_last_update; + else + remaining = 0; + GST_LOG_OBJECT (qtmux, "reserved duration remaining - reporting %" + G_GUINT64_FORMAT "(%" G_GUINT64_FORMAT " - %" G_GUINT64_FORMAT, + remaining, qtmux->reserved_duration_remaining, + qtmux->muxed_since_last_update); + g_value_set_uint64 (value, remaining); + } + break; + case PROP_RESERVED_MOOV_UPDATE_PERIOD: + g_value_set_uint64 (value, qtmux->reserved_moov_update_period); + break; + case PROP_RESERVED_BYTES_PER_SEC: + g_value_set_uint (value, qtmux->reserved_bytes_per_sec_per_trak); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -3771,6 +4277,15 @@ gst_qt_mux_set_property (GObject * object, } break; } + case PROP_RESERVED_MAX_DURATION: + qtmux->reserved_max_duration = g_value_get_uint64 (value); + break; + case PROP_RESERVED_MOOV_UPDATE_PERIOD: + qtmux->reserved_moov_update_period = g_value_get_uint64 (value); + break; + case PROP_RESERVED_BYTES_PER_SEC: + qtmux->reserved_bytes_per_sec_per_trak = g_value_get_uint (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; diff --git a/gst/isomp4/gstqtmux.h b/gst/isomp4/gstqtmux.h index 0e30eb4a06..3f2896e35d 100644 --- a/gst/isomp4/gstqtmux.h +++ b/gst/isomp4/gstqtmux.h @@ -128,6 +128,9 @@ struct _GstQTPad /* optional fragment index book-keeping */ AtomTFRA *tfra; + /* Set when tags are received, cleared when written to moov */ + gboolean tags_changed; + GstTagList *tags; /* if nothing is set, it won't be called */ @@ -148,7 +151,8 @@ typedef enum _GstQtMuxMode { GST_QT_MUX_MODE_MOOV_AT_END, GST_QT_MUX_MODE_FRAGMENTED, GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE, - GST_QT_MUX_MODE_FAST_START + GST_QT_MUX_MODE_FAST_START, + GST_QT_MUX_MODE_ROBUST_RECORDING } GstQtMuxMode; struct _GstQTMux @@ -168,14 +172,23 @@ struct _GstQTMux /* size of header (prefix, atoms (ftyp, possibly moov, mdat header)) */ guint64 header_size; - /* accumulated size of raw media data (a priori not including mdat header) */ + /* accumulated size of raw media data (not including mdat header) */ guint64 mdat_size; - /* position of mdat atom (for later updating) */ + /* position of the moov (for fragmented mode) or reserved moov atom + * area (for robust-muxing mode) */ + guint64 moov_pos; + /* position of mdat atom header (for later updating of size) in + * moov-at-end, fragmented and robust-muxing modes */ guint64 mdat_pos; /* keep track of the largest chunk to fine-tune brands */ GstClockTime longest_chunk; + /* Earliest timestamp across all pads/traks */ + GstClockTime first_ts; + /* Last DTS across all pads (= duration) */ + GstClockTime last_dts; + /* atom helper objects */ AtomsContext *context; AtomFTYP *ftyp; @@ -183,6 +196,10 @@ struct _GstQTMux GSList *extra_atoms; /* list of extra top-level atoms (e.g. UUID for xmp) * Stored as AtomInfo structs */ + /* Set when tags are received, cleared when written to moov */ + gboolean tags_changed; + + /* fragmented file index */ AtomMFRA *mfra; @@ -212,6 +229,30 @@ struct _GstQTMux * mode. */ gboolean streamable; + /* Requested target maximum duration */ + GstClockTime reserved_max_duration; + /* Estimate of remaining reserved header space (in ns of recording) */ + GstClockTime reserved_duration_remaining; + /* Multiplier for conversion from reserved_max_duration to bytes */ + guint reserved_bytes_per_sec_per_trak; + + /* Reserved minimum MOOV size in bytes + * This is converted from reserved_max_duration + * using the bytes/trak/sec estimate */ + guint32 reserved_moov_size; + /* Basic size of the moov (static headers + tags) */ + guint32 base_moov_size; + /* Size of the most recently generated moov header */ + guint32 last_moov_size; + /* True if the first moov in the ping-pong buffers + * is the active one. See gst_qt_mux_robust_recording_rewrite_moov() */ + gboolean reserved_moov_first_active; + + /* Tracking of periodic MOOV updates */ + GstClockTime last_moov_update; + GstClockTime reserved_moov_update_period; + GstClockTime muxed_since_last_update; + /* for request pad naming */ guint video_pads, audio_pads, subtitle_pads; };