isomp4/mux: add a fragment mode for initial moov with data
authorMatthew Waters <matthew@centricular.com>
Thu, 28 May 2020 09:40:24 +0000 (19:40 +1000)
committerMatthew Waters <matthew@centricular.com>
Mon, 21 Sep 2020 02:08:14 +0000 (12:08 +1000)
Used by some proprietary software for their fragmented files.

Adds some support for multi-stream fragmented files

Flow is as follows.
1. The first 'fragment' is written as a self-contained fragmented
   mdat+moov complete with an edit list and durations, tags, etc.
2. Subsequent fragments are written with a mdat+moof and each stream is
   interleaved as data arrives (currently ignoring the interleave-*
   properties).  data-offsets in both the traf and the trun ensure
   data is read from the correct place on demuxing.  Data/chunk offsets
   are also kept for writing out the final moov.
3. On finalisation, the initial moov is invalidated to a hoov and the
   size of the first mdat is extended to cover the entire file contents.
   Then a moov is written as regularly would in moov-at-end mode (the
   default).

This results in a file that is playable throughout while leaving a
finalised file on completion for players that do not understand
fragmented mp4.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-good/-/merge_requests/643>

docs/gst_plugins_cache.json
gst/isomp4/atoms.c
gst/isomp4/atoms.h
gst/isomp4/gstqtmux.c
gst/isomp4/gstqtmux.h
gst/isomp4/gstqtmuxmap.c
tests/check/elements/qtmux.c

index 56b7a8c..8407983 100644 (file)
                     }
                 ]
             },
+            "GstQTMuxFragmentMode": {
+                "kind": "enum",
+                "values": [
+                    {
+                        "desc": "dash-or-mss",
+                        "name": "Dash or Smoothstreaming",
+                        "value": "0"
+                    },
+                    {
+                        "desc": "first-moov-then-finalise",
+                        "name": "First MOOV Fragment Then Finalise",
+                        "value": "1"
+                    }
+                ]
+            },
             "GstQTMuxPad": {
                 "hierarchy": [
                     "GstQTMuxPad",
index e9671bb..d01426f 100644 (file)
@@ -2953,8 +2953,11 @@ atom_mvex_copy_data (AtomMVEX * mvex, guint8 ** buffer, guint64 * size,
     return 0;
   }
 
-  if (!atom_mehd_copy_data (&mvex->mehd, buffer, size, offset)) {
-    return 0;
+  if (mvex->mehd.fragment_duration > 0) {
+    /* only write mehd if we have anything extra to add */
+    if (!atom_mehd_copy_data (&mvex->mehd, buffer, size, offset)) {
+      return 0;
+    }
   }
 
   walker = g_list_first (mvex->trexs);
@@ -4457,6 +4460,25 @@ atom_moof_new (AtomsContext * context, guint32 sequence_number)
   return moof;
 }
 
+void
+atom_moof_set_base_offset (AtomMOOF * moof, guint64 offset)
+{
+  GList *trafs = moof->trafs;
+
+  if (offset == moof->traf_offset)
+    return;                     /* Nothing to do */
+
+  while (trafs) {
+    AtomTRAF *traf = (AtomTRAF *) trafs->data;
+
+    traf->tfhd.header.flags[2] |= TF_BASE_DATA_OFFSET;
+    traf->tfhd.base_data_offset = offset;
+    trafs = g_list_next (trafs);
+  }
+
+  moof->traf_offset = offset;
+}
+
 static void
 atom_trun_free (AtomTRUN * trun)
 {
@@ -4581,21 +4603,13 @@ atom_tfdt_copy_data (AtomTFDT * tfdt, guint8 ** buffer, guint64 * size,
 
 static guint64
 atom_trun_copy_data (AtomTRUN * trun, guint8 ** buffer, guint64 * size,
-    guint64 * offset, guint32 * data_offset)
+    guint64 * offset)
 {
   guint64 original_offset = *offset;
   guint32 flags, i;
 
   flags = atom_full_get_flags_as_uint (&trun->header);
 
-  /* if first trun in moof, forcibly add data_offset and record
-   * where it must be written later on */
-  if (data_offset && !*data_offset) {
-    flags |= TR_DATA_OFFSET;
-  } else {
-    flags &= ~TR_DATA_OFFSET;
-  }
-
   atom_full_set_flags_as_uint (&trun->header, flags);
 
   if (!atom_full_copy_data (&trun->header, buffer, size, offset)) {
@@ -4605,7 +4619,6 @@ atom_trun_copy_data (AtomTRUN * trun, guint8 ** buffer, guint64 * size,
   prop_copy_uint32 (trun->sample_count, buffer, size, offset);
 
   if (flags & TR_DATA_OFFSET) {
-    *data_offset = *offset;
     prop_copy_int32 (trun->data_offset, buffer, size, offset);
   }
   if (flags & TR_FIRST_SAMPLE_FLAGS)
@@ -4649,7 +4662,7 @@ atom_sdtp_copy_data (AtomSDTP * sdtp, guint8 ** buffer, guint64 * size,
 
 static guint64
 atom_traf_copy_data (AtomTRAF * traf, guint8 ** buffer, guint64 * size,
-    guint64 * offset, guint32 * data_offset)
+    guint64 * offset)
 {
   guint64 original_offset = *offset;
   GList *walker;
@@ -4663,11 +4676,9 @@ atom_traf_copy_data (AtomTRAF * traf, guint8 ** buffer, guint64 * size,
   if (!atom_tfdt_copy_data (&traf->tfdt, buffer, size, offset)) {
     return 0;
   }
-
   walker = g_list_first (traf->truns);
   while (walker != NULL) {
-    if (!atom_trun_copy_data ((AtomTRUN *) walker->data, buffer, size, offset,
-            data_offset)) {
+    if (!atom_trun_copy_data ((AtomTRUN *) walker->data, buffer, size, offset)) {
       return 0;
     }
     walker = g_list_next (walker);
@@ -4693,7 +4704,6 @@ atom_moof_copy_data (AtomMOOF * moof, guint8 ** buffer,
 {
   guint64 original_offset = *offset;
   GList *walker;
-  guint32 data_offset = 0;
 
   if (!atom_copy_data (&moof->header, buffer, size, offset))
     return 0;
@@ -4703,8 +4713,7 @@ atom_moof_copy_data (AtomMOOF * moof, guint8 ** buffer,
 
   walker = g_list_first (moof->trafs);
   while (walker != NULL) {
-    if (!atom_traf_copy_data ((AtomTRAF *) walker->data, buffer, size, offset,
-            &data_offset)) {
+    if (!atom_traf_copy_data ((AtomTRAF *) walker->data, buffer, size, offset)) {
       return 0;
     }
     walker = g_list_next (walker);
@@ -4712,12 +4721,6 @@ atom_moof_copy_data (AtomMOOF * moof, guint8 ** buffer,
 
   atom_write_size (buffer, size, offset, original_offset);
 
-  if (*buffer && data_offset) {
-    /* first trun needs a data-offset relative to moof start
-     *   = moof size + mdat prefix */
-    GST_WRITE_UINT32_BE (*buffer + data_offset, *offset - original_offset + 8);
-  }
-
   return *offset - original_offset;
 }
 
@@ -4797,21 +4800,62 @@ atom_sdtp_add_samples (AtomSDTP * sdtp, guint8 val)
   atom_array_append (&sdtp->entries, val, 256);
 }
 
+void
+atom_trun_set_offset (AtomTRUN * trun, gint32 offset)
+{
+  trun->header.flags[2] |= TR_DATA_OFFSET;
+  trun->data_offset = offset;
+}
+
+static gboolean
+atom_trun_can_append_samples_to_entry (AtomTRUN * trun,
+    TRUNSampleEntry * nentry, guint32 nsamples, guint32 delta, guint32 size,
+    guint32 flags, gint32 data_offset, gint64 pts_offset)
+{
+  if (pts_offset != 0)
+    return FALSE;
+  if (nentry->sample_flags != flags)
+    return FALSE;
+  if (trun->data_offset + nentry->sample_size != data_offset)
+    return FALSE;
+  if (nentry->sample_size != size)
+    return FALSE;
+  if (nentry->sample_duration != delta)
+    return FALSE;
+
+  return TRUE;
+}
+
+static void
+atom_trun_append_samples (AtomTRUN * trun, TRUNSampleEntry * nentry,
+    guint32 nsamples, guint32 delta, guint32 size)
+{
+  trun->sample_count += nsamples;
+}
+
 static void
-atom_trun_add_samples (AtomTRUN * trun, guint32 delta, guint32 size,
-    guint32 flags, gint64 pts_offset)
+atom_trun_add_samples (AtomTRUN * trun, guint32 nsamples, guint32 delta,
+    guint32 size, guint32 flags, gint64 pts_offset)
 {
-  TRUNSampleEntry nentry;
+  int i;
 
   if (pts_offset != 0)
     trun->header.flags[1] |= (TR_COMPOSITION_TIME_OFFSETS >> 8);
 
-  nentry.sample_duration = delta;
-  nentry.sample_size = size;
-  nentry.sample_flags = flags;
-  nentry.sample_composition_time_offset = pts_offset;
-  atom_array_append (&trun->entries, nentry, 256);
-  trun->sample_count++;
+  for (i = 0; i < nsamples; i++) {
+    TRUNSampleEntry nentry;
+
+    nentry.sample_duration = delta;
+    nentry.sample_size = size;
+    nentry.sample_flags = flags;
+    if (pts_offset != 0) {
+      nentry.sample_composition_time_offset = pts_offset + i * delta;
+    } else {
+      nentry.sample_composition_time_offset = 0;
+    }
+    atom_array_append (&trun->entries, nentry, 256);
+    trun->sample_count++;
+  }
 }
 
 static void
@@ -4851,41 +4895,67 @@ atom_traf_add_trun (AtomTRAF * traf, AtomTRUN * trun)
 }
 
 void
-atom_traf_add_samples (AtomTRAF * traf, guint32 delta, guint32 size,
-    gboolean sync, gint64 pts_offset, gboolean sdtp_sync)
+atom_traf_add_samples (AtomTRAF * traf, guint32 nsamples,
+    guint32 delta, guint32 size, gint32 data_offset, gboolean sync,
+    gint64 pts_offset, gboolean sdtp_sync)
 {
-  AtomTRUN *trun;
+  GList *l = NULL;
+  AtomTRUN *prev_trun, *trun = NULL;
+  TRUNSampleEntry *nentry = NULL;
   guint32 flags;
 
   /* 0x10000 is sample-is-difference-sample flag
    * low byte stuff is what ismv uses */
   flags = (sync ? 0x0 : 0x10000) | (sdtp_sync ? 0x40 : 0xc0);
 
-  if (G_UNLIKELY (!traf->truns)) {
-    trun = atom_trun_new ();
-    atom_traf_add_trun (traf, trun);
+  if (traf->truns) {
+    trun = g_list_last (traf->truns)->data;
+    nentry =
+        &atom_array_index (&trun->entries,
+        atom_array_get_len (&trun->entries) - 1);
+
+    if (!atom_trun_can_append_samples_to_entry (trun, nentry, nsamples, delta,
+            size, flags, data_offset, pts_offset)) {
+      /* if we can't add to the previous trun, write a new one */
+      trun = NULL;
+      nentry = NULL;
+    }
+  }
+  prev_trun = trun;
+
+  if (!traf->truns) {
     /* optimistic; indicate all defaults present in tfhd */
     traf->tfhd.header.flags[2] = TF_DEFAULT_SAMPLE_DURATION |
         TF_DEFAULT_SAMPLE_SIZE | TF_DEFAULT_SAMPLE_FLAGS;
     traf->tfhd.default_sample_duration = delta;
     traf->tfhd.default_sample_size = size;
     traf->tfhd.default_sample_flags = flags;
-    trun->first_sample_flags = flags;
   }
 
-  trun = traf->truns->data;
+  if (!trun) {
+    trun = atom_trun_new ();
+    atom_traf_add_trun (traf, trun);
+    trun->first_sample_flags = flags;
+    trun->data_offset = data_offset;
+    if (data_offset != 0)
+      trun->header.flags[2] |= TR_DATA_OFFSET;
+  }
 
   /* check if still matching defaults,
    * if not, abandon default and need entry for each sample */
-  if (traf->tfhd.default_sample_duration != delta) {
+  if (traf->tfhd.default_sample_duration != delta || prev_trun == trun) {
     traf->tfhd.header.flags[2] &= ~TF_DEFAULT_SAMPLE_DURATION;
-    trun->header.flags[1] |= (TR_SAMPLE_DURATION >> 8);
+    for (l = traf->truns; l; l = g_list_next (l)) {
+      ((AtomTRUN *) l->data)->header.flags[1] |= (TR_SAMPLE_DURATION >> 8);
+    }
   }
-  if (traf->tfhd.default_sample_size != size) {
+  if (traf->tfhd.default_sample_size != size || prev_trun == trun) {
     traf->tfhd.header.flags[2] &= ~TF_DEFAULT_SAMPLE_SIZE;
-    trun->header.flags[1] |= (TR_SAMPLE_SIZE >> 8);
+    for (l = traf->truns; l; l = g_list_next (l)) {
+      ((AtomTRUN *) l->data)->header.flags[1] |= (TR_SAMPLE_SIZE >> 8);
+    }
   }
-  if (traf->tfhd.default_sample_flags != flags) {
+  if (traf->tfhd.default_sample_flags != flags || prev_trun == trun) {
     if (trun->sample_count == 1) {
       /* at least will need first sample flag */
       traf->tfhd.default_sample_flags = flags;
@@ -4898,7 +4968,11 @@ atom_traf_add_samples (AtomTRAF * traf, guint32 delta, guint32 size,
     }
   }
 
-  atom_trun_add_samples (traf->truns->data, delta, size, flags, pts_offset);
+  if (prev_trun == trun) {
+    atom_trun_append_samples (trun, nentry, nsamples, delta, size);
+  } else {
+    atom_trun_add_samples (trun, nsamples, delta, size, flags, pts_offset);
+  }
 
   if (traf->sdtps)
     atom_sdtp_add_samples (traf->sdtps->data, 0x10 | ((flags & 0xff) >> 4));
@@ -4912,6 +4986,7 @@ atom_traf_get_sample_num (AtomTRAF * traf)
   if (G_UNLIKELY (!traf->truns))
     return 0;
 
+  /* FIXME: only one trun? */
   trun = traf->truns->data;
   return atom_array_get_len (&trun->entries);
 }
index b2587d9..7520473 100644 (file)
@@ -831,7 +831,7 @@ typedef struct _AtomTRAF
 
   AtomTFDT tfdt;
 
-  /* list of AtomTRUN */
+  /* list of AtomTRUN. */
   GList *truns;
   /* list of AtomSDTP */
   GList *sdtps;
@@ -845,6 +845,8 @@ typedef struct _AtomMOOF
 
   /* list of AtomTRAF */
   GList *trafs;
+
+  guint64 traf_offset;
 } AtomMOOF;
 
 
@@ -989,13 +991,15 @@ guint64    atom_stco64_copy_data       (AtomSTCO64 *atom, guint8 **buffer,
 AtomMOOF*  atom_moof_new               (AtomsContext *context, guint32 sequence_number);
 void       atom_moof_free              (AtomMOOF *moof);
 guint64    atom_moof_copy_data         (AtomMOOF *moof, guint8 **buffer, guint64 *size, guint64* offset);
+void       atom_moof_set_base_offset   (AtomMOOF * moof, guint64 offset);
 AtomTRAF * atom_traf_new               (AtomsContext * context, guint32 track_ID);
 void       atom_traf_free              (AtomTRAF * traf);
 void       atom_traf_set_base_decode_time (AtomTRAF * traf, guint64 base_decode_time);
-void       atom_traf_add_samples       (AtomTRAF * traf, guint32 delta,
-                                        guint32 size, gboolean sync, gint64 pts_offset,
-                                        gboolean sdtp_sync);
+void       atom_traf_add_samples       (AtomTRAF * traf, guint32 nsamples, guint32 delta,
+                                        guint32 size, gint32 data_offset, gboolean sync,
+                                        gint64 pts_offset, gboolean sdtp_sync);
 guint32    atom_traf_get_sample_num    (AtomTRAF * traf);
+void       atom_trun_set_offset        (AtomTRUN * trun, gint32 offset);
 void       atom_moof_add_traf          (AtomMOOF *moof, AtomTRAF *traf);
 
 AtomMFRA*  atom_mfra_new               (AtomsContext *context);
index 6a5c720..beecacf 100644 (file)
@@ -259,6 +259,33 @@ gst_qt_mux_dts_method_get_type (void)
   (gst_qt_mux_dts_method_get_type ())
 #endif
 
+static GType
+gst_qt_mux_fragment_mode_get_type (void)
+{
+  static GType gst_qt_mux_fragment_mode = 0;
+
+  if (!gst_qt_mux_fragment_mode) {
+    static const GEnumValue gst_qt_mux_fragment_modes[] = {
+      {GST_QT_MUX_FRAGMENT_DASH_OR_MSS, "Dash or Smoothstreaming",
+          "dash-or-mss"},
+      {GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE,
+          "First MOOV Fragment Then Finalise", "first-moov-then-finalise"},
+      /* internal only */
+      /* {GST_QT_MUX_FRAGMENT_STREAMABLE, "streamable", "Streamable (ISML only.  Deprecated elsewhere)"}, */
+      {0, NULL, NULL},
+    };
+
+    gst_qt_mux_fragment_mode =
+        g_enum_register_static ("GstQTMuxFragmentMode",
+        gst_qt_mux_fragment_modes);
+  }
+
+  return gst_qt_mux_fragment_mode;
+}
+
+#define GST_TYPE_QT_MUX_FRAGMENT_MODE \
+  (gst_qt_mux_fragment_mode_get_type ())
+
 enum
 {
   PROP_PAD_0,
@@ -370,6 +397,7 @@ enum
   PROP_MAX_RAW_AUDIO_DRIFT,
   PROP_START_GAP_THRESHOLD,
   PROP_FORCE_CREATE_TIMECODE_TRAK,
+  PROP_FRAGMENT_MODE,
 };
 
 /* some spare for header size as well */
@@ -396,6 +424,7 @@ enum
 #define DEFAULT_MAX_RAW_AUDIO_DRIFT 40 * GST_MSECOND
 #define DEFAULT_START_GAP_THRESHOLD 0
 #define DEFAULT_FORCE_CREATE_TIMECODE_TRAK FALSE
+#define DEFAULT_FRAGMENT_MODE GST_QT_MUX_FRAGMENT_DASH_OR_MSS
 
 static void gst_qt_mux_finalize (GObject * object);
 
@@ -440,7 +469,7 @@ static void gst_qt_mux_update_edit_lists (GstQTMux * qtmux);
 static GstElementClass *parent_class = NULL;
 
 static void
-gst_qt_mux_base_init (gpointer g_class)
+gst_qt_mux_subclass_base_init (gpointer g_class)
 {
   GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
   GstQTMuxClass *klass = (GstQTMuxClass *) g_class;
@@ -502,7 +531,7 @@ gst_qt_mux_base_init (gpointer g_class)
 }
 
 static void
-gst_qt_mux_class_init (GstQTMuxClass * klass)
+gst_qt_mux_subclass_class_init (GstQTMuxClass * klass)
 {
   GObjectClass *gobject_class;
   GstElementClass *gstelement_class;
@@ -651,6 +680,29 @@ gst_qt_mux_class_init (GstQTMuxClass * klass)
           DEFAULT_FORCE_CREATE_TIMECODE_TRAK,
           G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
 
+  /**
+   * GstQTMux:fragment-mode:
+   *
+   * Influence how fragmented files are produces.  Only has any affect when the
+   * the 'fragment-duration' property is set to a value greater than '0'
+   *
+   * Currently, two options exist:
+   * - "dash-or-mss": for the original fragmented mode that supports dash or
+   *   mocrosoft smoothstreaming with a single input stream
+   * - "first-moov-then-finalise" is a fragmented mode that will start with a
+   *   self-contained 'moov' atom fo the first fragment, then produce fragments.
+   *   When the file is finalised, the initial 'moov' is invalidated and a
+   *   new 'moov' is written covering the entire file.
+   *
+   * Since: 1.20
+   */
+  g_object_class_install_property (gobject_class, PROP_FRAGMENT_MODE,
+      g_param_spec_enum ("fragment-mode", "Fragment Mode",
+          "How to to write fragments to the file.  Only used when "
+          "\'fragment-duration\' is greather than 0",
+          GST_TYPE_QT_MUX_FRAGMENT_MODE, DEFAULT_FRAGMENT_MODE,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
   gstelement_class->request_new_pad =
       GST_DEBUG_FUNCPTR (gst_qt_mux_request_new_pad);
   gstelement_class->release_pad = GST_DEBUG_FUNCPTR (gst_qt_mux_release_pad);
@@ -665,6 +717,7 @@ gst_qt_mux_class_init (GstQTMuxClass * klass)
 
   gst_type_mark_as_plugin_api (GST_TYPE_QT_MUX_PAD, 0);
   gst_type_mark_as_plugin_api (GST_TYPE_QT_MUX_DTS_METHOD, 0);
+  gst_type_mark_as_plugin_api (GST_TYPE_QT_MUX_FRAGMENT_MODE, 0);
 }
 
 static void
@@ -861,7 +914,7 @@ gst_qt_mux_clip_running_time (GstAggregator * agg,
 }
 
 static void
-gst_qt_mux_init (GstQTMux * qtmux, GstQTMuxClass * qtmux_klass)
+gst_qt_mux_subclass_init (GstQTMux * qtmux, GstQTMuxClass * qtmux_klass)
 {
   /* properties set to default upon construction */
 
@@ -2283,18 +2336,9 @@ serialize_error:
 }
 
 static void
-gst_qt_mux_configure_moov (GstQTMux * qtmux)
+gst_qt_mux_configure_moov_full (GstQTMux * qtmux, gboolean fragmented,
+    guint32 timescale)
 {
-  gboolean fragmented = FALSE;
-  guint32 timescale;
-
-  GST_OBJECT_LOCK (qtmux);
-  timescale = qtmux->timescale;
-  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.
    * Let moov take care of this using its list of traks;
    * so that released pads are also included */
@@ -2306,6 +2350,22 @@ gst_qt_mux_configure_moov (GstQTMux * qtmux)
   atom_moov_update_duration (qtmux->moov);
 }
 
+static void
+gst_qt_mux_configure_moov (GstQTMux * qtmux)
+{
+  gboolean fragmented = FALSE;
+  guint32 timescale;
+
+  GST_OBJECT_LOCK (qtmux);
+  timescale = qtmux->timescale;
+  if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED
+      && qtmux->fragment_mode != GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE)
+    fragmented = TRUE;
+  GST_OBJECT_UNLOCK (qtmux);
+
+  gst_qt_mux_configure_moov_full (qtmux, fragmented, timescale);
+}
+
 static GstFlowReturn
 gst_qt_mux_send_moov (GstQTMux * qtmux, guint64 * _offset,
     guint64 padded_moov_size, gboolean mind_fast, gboolean fsync_after)
@@ -2813,8 +2873,7 @@ find_best_pad_prefill_start (GstQTMux * qtmux)
           || qtmux->current_chunk_size <= qtmux->interleave_bytes)
       && (qtmux->interleave_time == 0
           || qtmux->current_chunk_duration <= qtmux->interleave_time)
-      && qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED
-      && qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE) {
+      && qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED) {
 
     if (qtmux->current_pad->total_duration < qtmux->reserved_max_duration) {
       best_pad = qtmux->current_pad;
@@ -3041,10 +3100,11 @@ gst_qt_mux_start_file (GstQTMux * qtmux)
     goto invalid_isml;
 
   if (qtmux->fragment_duration > 0) {
-    if (qtmux->streamable)
-      qtmux->mux_mode = GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE;
-    else
-      qtmux->mux_mode = GST_QT_MUX_MODE_FRAGMENTED;
+    qtmux->mux_mode = GST_QT_MUX_MODE_FRAGMENTED;
+    if (qtmux->streamable
+        && qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_DASH_OR_MSS) {
+      qtmux->fragment_mode = GST_QT_MUX_FRAGMENT_STREAMABLE;
+    }
   } else if (qtmux->fast_start) {
     qtmux->mux_mode = GST_QT_MUX_MODE_FAST_START;
   } else if (reserved_max_duration != GST_CLOCK_TIME_NONE) {
@@ -3078,9 +3138,10 @@ gst_qt_mux_start_file (GstQTMux * qtmux)
       }
       break;
     case GST_QT_MUX_MODE_FAST_START:
-    case GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE:
       break;                    /* Don't need seekability, ignore */
     case GST_QT_MUX_MODE_FRAGMENTED:
+      if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_STREAMABLE)
+        break;
       if (!gst_qt_mux_downstream_is_seekable (qtmux)) {
         GST_WARNING_OBJECT (qtmux, "downstream is not seekable, but "
             "streamable=false. Will ignore that and create streamable output "
@@ -3408,31 +3469,46 @@ gst_qt_mux_start_file (GstQTMux * qtmux)
       ret = gst_qt_mux_send_buffer (qtmux, gst_buffer_new (), NULL, FALSE);
       break;
     case GST_QT_MUX_MODE_FRAGMENTED:
-    case GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE:
       ret = gst_qt_mux_prepare_and_send_ftyp (qtmux);
       if (ret != GST_FLOW_OK)
         break;
-      /* 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);
-      gst_qt_mux_setup_metadata (qtmux);
-      ret = gst_qt_mux_send_moov (qtmux, &qtmux->header_size, 0, FALSE, FALSE);
-      if (ret != GST_FLOW_OK)
-        return ret;
-      /* extra atoms */
-      ret =
-          gst_qt_mux_send_extra_atoms (qtmux, TRUE, &qtmux->header_size, FALSE);
-      if (ret != GST_FLOW_OK)
-        break;
-      /* prepare index if not streamable */
-      if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED)
+      qtmux->fragment_sequence = 0;
+      if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE) {
+        /* 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,
+            FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+      } else {
+        /* store the moov pos so we can update the duration later
+         * in non-streamable mode */
+        qtmux->moov_pos = qtmux->header_size;
+
+        /* prepare moov and/or tags */
+        qtmux->fragment_sequence++;
+        gst_qt_mux_configure_moov (qtmux);
+        gst_qt_mux_setup_metadata (qtmux);
+        ret =
+            gst_qt_mux_send_moov (qtmux, &qtmux->header_size, 0, FALSE, FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+        /* extra atoms */
+        ret =
+            gst_qt_mux_send_extra_atoms (qtmux, TRUE, &qtmux->header_size,
+            FALSE);
+        if (ret != GST_FLOW_OK)
+          break;
+      }
+      /* prepare index if not streamable, or overwriting with moov */
+      if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_DASH_OR_MSS)
         qtmux->mfra = atom_mfra_new (qtmux->context);
       break;
   }
@@ -3718,7 +3794,8 @@ gst_qt_mux_stop_file (GstQTMux * qtmux)
   if ((ret = gst_qt_mux_send_last_buffers (qtmux)) != GST_FLOW_OK)
     return ret;
 
-  if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE) {
+  if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED
+      && qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_STREAMABLE) {
     /* Streamable mode; no need to write duration or MFRA */
     GST_DEBUG_OBJECT (qtmux, "streamable file; nothing to stop");
     return GST_FLOW_OK;
@@ -3750,33 +3827,81 @@ gst_qt_mux_stop_file (GstQTMux * qtmux)
   switch (qtmux->mux_mode) {
     case GST_QT_MUX_MODE_FRAGMENTED:{
       GstSegment segment;
-      guint8 *data = NULL;
       GstBuffer *buf;
+      GstClockTime duration;
 
-      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;
+      if (qtmux->mfra) {
+        guint8 *data = NULL;
+
+        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_round (qtmux->last_dts, qtmux->timescale,
+      duration = gst_util_uint64_scale_round (qtmux->last_dts, qtmux->timescale,
           GST_SECOND);
+
       GST_DEBUG_OBJECT (qtmux,
-          "rewriting moov with mvex duration %" GST_TIME_FORMAT,
+          "writing moov with mvhd/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_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
-      /* no need to seek back */
-      return gst_qt_mux_send_moov (qtmux, NULL, 0, FALSE, FALSE);
+      if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE) {
+        /* seek and overwrite the original moov with an invalid atom */
+        /* XXX: assumes an extended size atom is not used for the moov */
+
+        qtmux->moov->mvhd.time_info.duration = duration;
+
+        gst_segment_init (&segment, GST_FORMAT_BYTES);
+        segment.start = qtmux->moov_pos + 4;    /* skip the size bytes */
+        gst_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
+
+        /* invalidate the previous moov */
+        buf = gst_buffer_new_wrapped (g_strdup ("h"), 1);
+        ret = gst_qt_mux_send_buffer (qtmux, buf, NULL, FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+
+        /* we want to rewrite the first mdat to cover the entire data before
+         * this moov */
+        qtmux->mdat_size = qtmux->header_size - qtmux->mdat_pos - 16;
+
+        gst_segment_init (&segment, GST_FORMAT_BYTES);
+        segment.start = qtmux->mdat_pos;
+        gst_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
+
+        ret = gst_qt_mux_update_mdat_size (qtmux, qtmux->mdat_pos,
+            qtmux->mdat_size, NULL, FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+
+        /* Then write the moov atom as in moov-at-end *without* updating the
+         * mdat size */
+        gst_segment_init (&segment, GST_FORMAT_BYTES);
+        segment.start = qtmux->header_size;
+        gst_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
+
+        /* revert back to moov-at-end assumptions where header_size is the
+         * size up to the first byte of data in the mdat */
+        qtmux->header_size = qtmux->mdat_pos + 16;
+        break;
+      } else {
+        qtmux->moov->mvex.mehd.fragment_duration = duration;
+
+        /* seek and rewrite the header */
+        gst_segment_init (&segment, GST_FORMAT_BYTES);
+        segment.start = qtmux->moov_pos;
+        gst_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
+        /* no need to seek back */
+        return gst_qt_mux_send_moov (qtmux, NULL, 0, FALSE, FALSE);
+      }
     }
     case GST_QT_MUX_MODE_ROBUST_RECORDING:{
       ret = gst_qt_mux_robust_recording_rewrite_moov (qtmux);
@@ -4054,6 +4179,10 @@ gst_qt_mux_stop_file (GstQTMux * qtmux)
         return ret;
       break;
     }
+    case GST_QT_MUX_MODE_FRAGMENTED:
+      g_assert (qtmux->fragment_mode ==
+          GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE);
+      break;
     default:
       g_assert_not_reached ();
   }
@@ -4074,14 +4203,48 @@ ftyp_error:
   }
 }
 
+static gboolean
+gst_qtmux_pad_update_fragment_duration (GstElement * element, GstPad * pad,
+    gpointer user_data)
+{
+  GstQTMux *qtmux = (GstQTMux *) element;
+  GstQTMuxPad *qt_pad = GST_QT_MUX_PAD (pad);
+
+  qt_pad->fragment_duration = gst_util_uint64_scale (qtmux->fragment_duration,
+      atom_trak_get_timescale (qt_pad->trak), 1000);
+
+  return TRUE;
+}
+
+static gboolean
+gst_qtmux_pad_collect_traf (GstElement * element, GstPad * pad,
+    gpointer user_data)
+{
+  GstQTMuxPad *qt_pad = GST_QT_MUX_PAD (pad);
+  AtomMOOF *moof = user_data;
+
+  GST_TRACE_OBJECT (pad, "adding traf %p to moof %p", qt_pad->traf, moof);
+
+  /* takes ownership */
+  if (qt_pad->traf)
+    atom_moof_add_traf (moof, qt_pad->traf);
+  qt_pad->traf = NULL;
+
+  return TRUE;
+}
+
 static GstFlowReturn
 gst_qt_mux_pad_fragment_add_buffer (GstQTMux * qtmux, GstQTMuxPad * pad,
     GstBuffer * buf, gboolean force, guint32 nsamples, gint64 dts,
-    guint32 delta, guint32 size, gboolean sync, gint64 pts_offset)
+    guint32 delta, guint32 size, guint64 chunk_offset, gboolean sync,
+    gint64 pts_offset)
 {
   GstFlowReturn ret = GST_FLOW_OK;
   guint index = 0;
 
+  GST_LOG_OBJECT (pad, "%p %u %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT,
+      pad->traf, force, qtmux->current_chunk_offset, chunk_offset);
+
   /* setup if needed */
   if (G_UNLIKELY (!pad->traf || force))
     goto init;
@@ -4091,65 +4254,230 @@ flush:
    * or at new keyframe if we should be minding those in the first place */
   if (G_UNLIKELY (force || (sync && pad->sync) ||
           pad->fragment_duration < (gint64) delta)) {
-    AtomMOOF *moof;
-    guint64 size = 0, offset = 0;
-    guint8 *data = NULL;
-    GstBuffer *buffer;
-    guint i, total_size;
-
-    /* now we know where moof ends up, update offset in tfra */
-    if (pad->tfra)
-      atom_tfra_update_offset (pad->tfra, qtmux->header_size);
-
-    moof = atom_moof_new (qtmux->context, qtmux->fragment_sequence);
-    /* takes ownership */
-    atom_moof_add_traf (moof, pad->traf);
-    pad->traf = NULL;
-    atom_moof_copy_data (moof, &data, &size, &offset);
-    buffer = _gst_buffer_new_take_data (data, offset);
-    GST_LOG_OBJECT (qtmux, "writing moof size %" G_GSIZE_FORMAT,
-        gst_buffer_get_size (buffer));
-    ret = gst_qt_mux_send_buffer (qtmux, buffer, &qtmux->header_size, FALSE);
-
-    atom_moof_free (moof);
 
-    if (ret != GST_FLOW_OK)
-      goto moof_send_error;
+    if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE) {
+      if (qtmux->fragment_sequence == 0) {
+        /* the first fragment which we write as a moov */
+        GstSegment segment;
+        guint64 orig_offset;
+        guint64 offset = orig_offset = qtmux->mdat_pos + 16 + qtmux->mdat_size;
+        guint64 chunk_increase;
+        AtomMOOF *moof;
+
+        GST_LOG_OBJECT (qtmux, "current file offset calculated to be %"
+            G_GUINT64_FORMAT " based on mdat pos %" G_GUINT64_FORMAT
+            " and size %" G_GUINT64_FORMAT, offset, qtmux->mdat_pos,
+            qtmux->mdat_size);
+
+        moof = atom_moof_new (qtmux->context, qtmux->fragment_sequence);
+        gst_element_foreach_sink_pad (GST_ELEMENT (qtmux),
+            gst_qtmux_pad_collect_traf, moof);
+        atom_moof_free (moof);
+
+        ret = gst_qt_mux_update_mdat_size (qtmux, qtmux->mdat_pos,
+            qtmux->mdat_size, NULL, FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
 
-    /* and actual data */
-    total_size = 0;
-    for (i = 0; i < atom_array_get_len (&pad->fragment_buffers); i++) {
-      total_size +=
-          gst_buffer_get_size (atom_array_index (&pad->fragment_buffers, i));
-    }
+        /* seek back to the end of the file */
+        gst_segment_init (&segment, GST_FORMAT_BYTES);
+        segment.start = qtmux->moov_pos = offset;
+        gst_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
 
-    GST_LOG_OBJECT (qtmux, "writing %d buffers, total_size %d",
-        atom_array_get_len (&pad->fragment_buffers), total_size);
-    ret = gst_qt_mux_send_mdat_header (qtmux, &qtmux->header_size, total_size,
-        FALSE, FALSE);
-    if (ret != GST_FLOW_OK)
-      goto mdat_header_send_error;
+        /* update moov data */
+        gst_qt_mux_update_global_statistics (qtmux);
+        gst_qt_mux_configure_moov_full (qtmux, TRUE, qtmux->timescale);
+        gst_qt_mux_update_edit_lists (qtmux);
+        gst_qt_mux_setup_metadata (qtmux);
+        /* chunk offset is the offset to the first byte inside the mdat */
+        atom_moov_chunks_set_offset (qtmux->moov, qtmux->mdat_pos + 16);
 
-    for (index = 0; index < atom_array_get_len (&pad->fragment_buffers);
-        index++) {
-      GST_DEBUG_OBJECT (qtmux, "sending fragment %p",
-          atom_array_index (&pad->fragment_buffers, index));
+        ret = gst_qt_mux_send_moov (qtmux, &offset, 0, TRUE, FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+
+        /* for the continuation in fragments, header_size is the tracking write
+         * position */
+        qtmux->header_size = offset;
+        qtmux->moof_mdat_pos = 0;
+
+        chunk_increase = offset - orig_offset + 16;
+        GST_LOG_OBJECT (qtmux, "We think we have written %" G_GUINT64_FORMAT
+            " including a moov and mdat header of %" G_GUINT64_FORMAT
+            ". mangling this buffer's chunk offset from %" G_GUINT64_FORMAT
+            " to %" G_GUINT64_FORMAT, qtmux->header_size,
+            offset - orig_offset + 16, chunk_offset,
+            chunk_offset + chunk_increase);
+        chunk_offset += chunk_increase;
+        /* this is the offset for the next chunk */
+        qtmux->current_chunk_offset +=
+            qtmux->current_chunk_size + chunk_increase;
+        qtmux->current_chunk_size = 0;
+        GST_LOG_OBJECT (qtmux, "change next chunk offset to %" G_GUINT64_FORMAT
+            " and size to %" G_GUINT64_FORMAT, qtmux->current_chunk_offset,
+            qtmux->current_chunk_size);
+
+        gst_element_foreach_sink_pad (GST_ELEMENT (qtmux),
+            gst_qtmux_pad_update_fragment_duration, NULL);
+      } else {
+        AtomMOOF *moof;
+        guint64 size = 0, offset = 0;
+        guint8 *data = NULL;
+        GstBuffer *moof_buffer;
+        guint64 moof_size = 0;
+        GstSegment segment;
+        guint64 chunk_increase;
+
+        /* rewrite the mdat header */
+        ret = gst_qt_mux_update_mdat_size (qtmux, qtmux->moof_mdat_pos,
+            qtmux->header_size - qtmux->moof_mdat_pos - 16, NULL, FALSE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+
+        /* reseek back to the current position */
+        gst_segment_init (&segment, GST_FORMAT_BYTES);
+        segment.start = qtmux->header_size;
+        gst_aggregator_update_segment (GST_AGGREGATOR (qtmux), &segment);
+
+        moof = atom_moof_new (qtmux->context, qtmux->fragment_sequence);
+        gst_element_foreach_sink_pad (GST_ELEMENT (qtmux),
+            gst_qtmux_pad_collect_traf, moof);
+        atom_moof_set_base_offset (moof, qtmux->moof_mdat_pos);
+        atom_moof_copy_data (moof, &data, &size, &offset);
+        moof_buffer = _gst_buffer_new_take_data (data, offset);
+        moof_size = gst_buffer_get_size (moof_buffer);
+
+        atom_moof_free (moof);
+        /* now we know where moof ends up, update offset in tfra */
+        if (pad->tfra)
+          atom_tfra_update_offset (pad->tfra, qtmux->header_size);
+
+        GST_LOG_OBJECT (qtmux, "writing moof of size %" G_GUINT64_FORMAT,
+            moof_size);
+        ret =
+            gst_qt_mux_send_buffer (qtmux, moof_buffer, &qtmux->header_size,
+            FALSE);
+        if (ret != GST_FLOW_OK)
+          goto moof_send_error;
+        qtmux->moof_mdat_pos = 0;
+
+        /* if we are writing a final moov, then we need to increase our chunk
+         * offsets to include the moof/mdat headers that were just written so
+         * so that they are correctly skipped over.
+         */
+        chunk_increase = moof_size + 16;
+        GST_LOG_OBJECT (qtmux, "We think we have currently written %"
+            G_GUINT64_FORMAT " including a moof of %" G_GUINT64_FORMAT
+            " mangling this buffer's chunk offset from %" G_GUINT64_FORMAT
+            " to %" G_GUINT64_FORMAT, qtmux->header_size, moof_size,
+            chunk_offset, chunk_offset + chunk_increase);
+        chunk_offset += chunk_increase;
+        /* this is the offset for the next chunk */
+        qtmux->current_chunk_offset +=
+            qtmux->current_chunk_size + chunk_increase;
+        qtmux->current_chunk_size = 0;
+        GST_LOG_OBJECT (qtmux, "change next chunk offset to %" G_GUINT64_FORMAT
+            " and size to %" G_GUINT64_FORMAT, qtmux->current_chunk_offset,
+            qtmux->current_chunk_size);
+
+        /* if we are are generating a moof, it is for all streams */
+        gst_element_foreach_sink_pad (GST_ELEMENT (qtmux),
+            gst_qtmux_pad_update_fragment_duration, NULL);
+      }
+    } else {
+      /* not moov-related. writes out moof then mdat for a single stream only */
+      AtomMOOF *moof;
+      guint64 size = 0, offset = 0;
+      guint8 *data = NULL;
+      GstBuffer *moof_buffer;
+      guint i, total_size;
+      AtomTRUN *first_trun;
+
+      total_size = 0;
+      for (i = 0; i < atom_array_get_len (&pad->fragment_buffers); i++) {
+        total_size +=
+            gst_buffer_get_size (atom_array_index (&pad->fragment_buffers, i));
+      }
+
+      moof = atom_moof_new (qtmux->context, qtmux->fragment_sequence);
+      /* takes ownership */
+      atom_moof_add_traf (moof, pad->traf);
+      /* write the offset into the first 'trun'.  All other truns are assumed
+       * to follow on from this trun.  skip over the mdat header (+8) */
+      atom_moof_copy_data (moof, &data, &size, &offset);
+      first_trun = (AtomTRUN *) pad->traf->truns->data;
+      atom_trun_set_offset (first_trun, size + 8);
+      pad->traf = NULL;
+      size = offset = 0;
+      atom_moof_copy_data (moof, &data, &size, &offset);
+      moof_buffer = _gst_buffer_new_take_data (data, offset);
+
+      atom_moof_free (moof);
+
+      /* now we know where moof ends up, update offset in tfra */
+      if (pad->tfra)
+        atom_tfra_update_offset (pad->tfra, qtmux->header_size);
+
+      GST_LOG_OBJECT (qtmux, "writing moof size %" G_GSIZE_FORMAT,
+          gst_buffer_get_size (moof_buffer));
       ret =
-          gst_qt_mux_send_buffer (qtmux,
-          atom_array_index (&pad->fragment_buffers, index), &qtmux->header_size,
+          gst_qt_mux_send_buffer (qtmux, moof_buffer, &qtmux->header_size,
           FALSE);
       if (ret != GST_FLOW_OK)
-        goto fragment_buf_send_error;
-    }
+        goto moof_send_error;
+
+      GST_LOG_OBJECT (qtmux, "writing %d buffers, total_size %d",
+          atom_array_get_len (&pad->fragment_buffers), total_size);
+
+      ret = gst_qt_mux_send_mdat_header (qtmux, &qtmux->header_size, total_size,
+          FALSE, FALSE);
+      if (ret != GST_FLOW_OK)
+        goto mdat_header_send_error;
 
+      for (index = 0; index < atom_array_get_len (&pad->fragment_buffers);
+          index++) {
+        GST_DEBUG_OBJECT (qtmux, "sending fragment %p",
+            atom_array_index (&pad->fragment_buffers, index));
+        ret =
+            gst_qt_mux_send_buffer (qtmux,
+            atom_array_index (&pad->fragment_buffers, index),
+            &qtmux->header_size, FALSE);
+        if (ret != GST_FLOW_OK)
+          goto fragment_buf_send_error;
+      }
+
+    }
     atom_array_clear (&pad->fragment_buffers);
     qtmux->fragment_sequence++;
     force = FALSE;
   }
 
 init:
-  if (G_UNLIKELY (!pad->traf)) {
-    GST_LOG_OBJECT (qtmux, "setting up new fragment");
+  if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE
+      && qtmux->fragment_sequence == 0) {
+    atom_trak_add_samples (pad->trak, nsamples, (gint32) delta, size,
+        chunk_offset, sync, pts_offset);
+
+    ret = gst_qt_mux_send_buffer (qtmux, buf, &qtmux->mdat_size, TRUE);
+    if (ret != GST_FLOW_OK)
+      return ret;
+    buf = NULL;
+
+    if (G_UNLIKELY (force))
+      goto flush;
+
+    if (!pad->traf) {
+      pad->traf = atom_traf_new (qtmux->context, atom_trak_get_id (pad->trak));
+      pad->fragment_duration = gst_util_uint64_scale (qtmux->fragment_duration,
+          atom_trak_get_timescale (pad->trak), 1000);
+    }
+    pad->fragment_duration -= delta;
+
+    return ret;
+  } else if (G_UNLIKELY (!pad->traf)) {
+    GstClockTime first_dts = 0, current_dts;
+    gint64 first_qt_dts;
+    GST_LOG_OBJECT (pad, "setting up new fragment");
     pad->traf = atom_traf_new (qtmux->context, atom_trak_get_id (pad->trak));
     atom_array_init (&pad->fragment_buffers, 512);
     pad->fragment_duration = gst_util_uint64_scale (qtmux->fragment_duration,
@@ -4159,14 +4487,55 @@ init:
       pad->tfra = atom_tfra_new (qtmux->context, atom_trak_get_id (pad->trak));
       atom_mfra_add_tfra (qtmux->mfra, pad->tfra);
     }
-    atom_traf_set_base_decode_time (pad->traf, dts);
-  }
+    if (GST_CLOCK_TIME_IS_VALID (pad->first_dts))
+      first_dts = pad->first_dts;
+
+    current_dts =
+        gst_util_uint64_scale (dts, GST_SECOND,
+        atom_trak_get_timescale (pad->trak));
+    first_qt_dts =
+        gst_util_uint64_scale (first_dts, atom_trak_get_timescale (pad->trak),
+        GST_SECOND);
+    GST_DEBUG_OBJECT (pad, "calculating base decode time with first dts %"
+        G_GINT64_FORMAT " (%" GST_TIME_FORMAT ") and current dts %"
+        G_GINT64_FORMAT " (%" GST_TIME_FORMAT ") of %" G_GINT64_FORMAT " (%"
+        GST_STIME_FORMAT ")", first_qt_dts, GST_TIME_ARGS (first_dts), dts,
+        GST_TIME_ARGS (current_dts), dts - first_qt_dts,
+        GST_STIME_ARGS (current_dts - first_dts));
+    atom_traf_set_base_decode_time (pad->traf, dts - first_qt_dts);
+  }
+
+  if (qtmux->fragment_mode == GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE) {
+    if (qtmux->fragment_sequence > 0 && !force) {
+      if (qtmux->moof_mdat_pos == 0) {
+        /* send temporary mdat */
+        qtmux->moof_mdat_pos = qtmux->header_size;
+        ret = gst_qt_mux_send_mdat_header (qtmux, &qtmux->header_size, 0,
+            TRUE, FALSE);
+        if (ret != GST_FLOW_OK)
+          goto mdat_header_send_error;
+      }
+
+      if (buf) {
+        atom_trak_add_samples (pad->trak, nsamples, (gint32) delta, size,
+            chunk_offset, sync, pts_offset);
+        atom_traf_add_samples (pad->traf, nsamples, delta, size,
+            qtmux->header_size - qtmux->moof_mdat_pos, sync, pts_offset,
+            pad->sync && sync);
 
-  /* add buffer and metadata */
-  atom_traf_add_samples (pad->traf, delta, size, sync, pts_offset,
-      pad->sync && sync);
-  GST_LOG_OBJECT (qtmux, "adding buffer %p to fragments", buf);
-  atom_array_append (&pad->fragment_buffers, g_steal_pointer (&buf), 256);
+        ret = gst_qt_mux_send_buffer (qtmux, buf, &qtmux->header_size, TRUE);
+        if (ret != GST_FLOW_OK)
+          return ret;
+        buf = NULL;
+      }
+    }
+  } else {
+    /* add buffer and metadata */
+    atom_traf_add_samples (pad->traf, nsamples, delta, size, 0, sync,
+        pts_offset, pad->sync && sync);
+    GST_LOG_OBJECT (qtmux, "adding buffer %p to fragments", buf);
+    atom_array_append (&pad->fragment_buffers, g_steal_pointer (&buf), 256);
+  }
   pad->fragment_duration -= delta;
 
   if (pad->tfra) {
@@ -4476,11 +4845,10 @@ gst_qt_mux_register_and_push_sample (GstQTMux * qtmux, GstQTMuxPad * pad,
         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:
       /* ensure that always sync samples are marked as such */
       ret = gst_qt_mux_pad_fragment_add_buffer (qtmux, pad, buffer,
           is_last_buffer, nsamples, last_dts, (gint32) scaled_duration,
-          sample_size, !pad->sync || sync, pts_offset);
+          sample_size, chunk_offset, !pad->sync || sync, pts_offset);
       break;
   }
 
@@ -4500,6 +4868,7 @@ gst_qt_mux_register_buffer_in_chunk (GstQTMux * qtmux, GstQTMuxPad * pad,
   pad->total_duration += duration;
   /* for keeping track of where we are in chunk;
    * ensures that data really is located as recorded in atoms */
+
   qtmux->current_chunk_size += buffer_size;
   qtmux->current_chunk_duration += duration;
 }
@@ -4533,6 +4902,8 @@ gst_qt_mux_check_and_update_timecode (GstQTMux * qtmux, GstQTMuxPad * pad,
 
   /* This means we never got a timecode before */
   if (pad->first_tc == NULL) {
+    guint64 *offset;
+
 #ifndef GST_DISABLE_GST_DEBUG
     gchar *tc_str = gst_video_time_code_to_string (tc);
     GST_DEBUG_OBJECT (qtmux, "Found first timecode %s", tc_str);
@@ -4540,6 +4911,13 @@ gst_qt_mux_check_and_update_timecode (GstQTMux * qtmux, GstQTMuxPad * pad,
 #endif
     g_assert (pad->tc_trak == NULL);
     pad->first_tc = gst_video_time_code_copy (tc);
+
+    if (qtmux->mux_mode == GST_QT_MUX_MODE_FRAGMENTED
+        && qtmux->fragment_sequence > 0) {
+      offset = &qtmux->header_size;
+    } else {
+      offset = &qtmux->mdat_size;
+    }
     /* If frames are out of order, the frame we're currently getting might
      * not be the first one. Just write a 0 timecode for now and wait
      * until we receive a timecode that's lower than the current one */
@@ -4547,7 +4925,7 @@ gst_qt_mux_check_and_update_timecode (GstQTMux * qtmux, GstQTMuxPad * pad,
       pad->first_pts = GST_BUFFER_PTS (buf);
       frames_since_daily_jam = 0;
       /* Position to rewrite */
-      pad->tc_pos = qtmux->mdat_size;
+      pad->tc_pos = *offset;
     } else {
       frames_since_daily_jam =
           gst_video_time_code_frames_since_daily_jam (pad->first_tc);
@@ -4567,8 +4945,8 @@ gst_qt_mux_check_and_update_timecode (GstQTMux * qtmux, GstQTMuxPad * pad,
     szret = gst_buffer_fill (tc_buf, 0, &frames_since_daily_jam, 4);
     g_assert (szret == 4);
 
-    atom_trak_add_samples (pad->tc_trak, 1, 1, 4, qtmux->mdat_size, FALSE, 0);
-    ret = gst_qt_mux_send_buffer (qtmux, tc_buf, &qtmux->mdat_size, TRUE);
+    atom_trak_add_samples (pad->tc_trak, 1, 1, 4, *offset, FALSE, 0);
+    ret = gst_qt_mux_send_buffer (qtmux, tc_buf, offset, TRUE);
 
     /* Need to reset the current chunk (of the previous pad) here because
      * some other data was written now above, and the pad has to start a
@@ -4781,7 +5159,9 @@ gst_qt_mux_add_buffer (GstQTMux * qtmux, GstQTMuxPad * pad, GstBuffer * buf)
       atom_trak_get_timescale (pad->trak), GST_SECOND);
 
   /* fragments only deal with 1 buffer == 1 chunk (== 1 sample) */
-  if (pad->sample_size && !qtmux->fragment_sequence) {
+  if (pad->sample_size && (qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED
+          || qtmux->fragment_mode ==
+          GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE)) {
     GstClockTime expected_timestamp;
 
     /* Constant size packets: usually raw audio (with many samples per
@@ -5151,8 +5531,7 @@ find_best_pad (GstQTMux * qtmux)
           || qtmux->current_chunk_size <= qtmux->interleave_bytes)
       && (qtmux->interleave_time == 0
           || qtmux->current_chunk_duration <= qtmux->interleave_time)
-      && qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED
-      && qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED_STREAMABLE) {
+      && qtmux->mux_mode != GST_QT_MUX_MODE_FRAGMENTED) {
     GstBuffer *tmp_buf =
         gst_aggregator_pad_peek_buffer (GST_AGGREGATOR_PAD
         (qtmux->current_pad));
@@ -6686,6 +7065,13 @@ gst_qt_mux_get_property (GObject * object,
     case PROP_FORCE_CREATE_TIMECODE_TRAK:
       g_value_set_boolean (value, qtmux->force_create_timecode_trak);
       break;
+    case PROP_FRAGMENT_MODE:{
+      GstQTMuxFragmentMode mode = qtmux->fragment_mode;
+      if (mode == GST_QT_MUX_FRAGMENT_STREAMABLE)
+        mode = GST_QT_MUX_FRAGMENT_DASH_OR_MSS;
+      g_value_set_enum (value, mode);
+      break;
+    }
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -6788,6 +7174,12 @@ gst_qt_mux_set_property (GObject * object,
       qtmux->context->force_create_timecode_trak =
           qtmux->force_create_timecode_trak;
       break;
+    case PROP_FRAGMENT_MODE:{
+      GstQTMuxFragmentMode mode = g_value_get_enum (value);
+      if (mode != GST_QT_MUX_FRAGMENT_STREAMABLE)
+        qtmux->fragment_mode = mode;
+      break;
+    }
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -6820,19 +7212,31 @@ gst_qt_mux_stop (GstAggregator * agg)
   return TRUE;
 }
 
+G_DEFINE_TYPE (GstQTMux, gst_qt_mux, GST_TYPE_AGGREGATOR);
+
+static void
+gst_qt_mux_class_init (GstQTMuxClass * klass)
+{
+}
+
+static void
+gst_qt_mux_init (GstQTMux * qtmux)
+{
+}
+
 gboolean
 gst_qt_mux_register (GstPlugin * plugin)
 {
   GTypeInfo typeinfo = {
     sizeof (GstQTMuxClass),
-    (GBaseInitFunc) gst_qt_mux_base_init,
+    (GBaseInitFunc) gst_qt_mux_subclass_base_init,
     NULL,
-    (GClassInitFunc) gst_qt_mux_class_init,
+    (GClassInitFunc) gst_qt_mux_subclass_class_init,
     NULL,
     NULL,
     sizeof (GstQTMux),
     0,
-    (GInstanceInitFunc) gst_qt_mux_init,
+    (GInstanceInitFunc) gst_qt_mux_subclass_init,
   };
   static const GInterfaceInfo tag_setter_info = {
     NULL, NULL, NULL
@@ -6882,8 +7286,8 @@ gst_qt_mux_register (GstPlugin * plugin)
 
     /* create the type now */
     type =
-        g_type_register_static (GST_TYPE_AGGREGATOR, prop->type_name, &typeinfo,
-        0);
+        g_type_register_static (gst_qt_mux_get_type (), prop->type_name,
+        &typeinfo, 0);
     g_type_set_qdata (type, GST_QT_MUX_PARAMS_QDATA, (gpointer) params);
     g_type_add_interface_static (type, GST_TYPE_TAG_SETTER, &tag_setter_info);
     g_type_add_interface_static (type, GST_TYPE_TAG_XMP_WRITER,
index 4c635c9..a90b8b8 100644 (file)
@@ -197,12 +197,26 @@ typedef enum _GstQTMuxState
 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_ROBUST_RECORDING,
     GST_QT_MUX_MODE_ROBUST_RECORDING_PREFILL,
 } GstQtMuxMode;
 
+/**
+ * GstQTMuxFragmentMode:
+ * GST_QT_MUX_FRAGMENT_DASH_OR_MSS: dash-or-mss
+ * GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE: first-moov-then-finalise
+ * GST_QT_MUX_FRAGMENT_STREAMABLE: streamable (private value)
+ *
+ * Since: 1.20
+ */
+typedef enum _GstQTMuxFragmentMode
+{
+  GST_QT_MUX_FRAGMENT_DASH_OR_MSS = 0,
+  GST_QT_MUX_FRAGMENT_FIRST_MOOV_THEN_FINALISE,
+  GST_QT_MUX_FRAGMENT_STREAMABLE = G_MAXUINT32, /* internal value */
+} GstQTMuxFragmentMode;
+
 struct _GstQTMux
 {
   GstAggregator parent;
@@ -213,6 +227,9 @@ struct _GstQTMux
   /* Mux mode, inferred from property
    * set in gst_qt_mux_start_file() */
   GstQtMuxMode mux_mode;
+  /* fragment_mode, controls how fragments are created.  Only if
+   * @mux_mode == GST_QT_MUX_MODE_FRAGMENTED */
+  GstQTMuxFragmentMode fragment_mode;
 
   /* size of header (prefix, atoms (ftyp, possibly moov, mdat header)) */
   guint64 header_size;
@@ -224,6 +241,10 @@ struct _GstQTMux
   /* position of mdat atom header (for later updating of size) in
    * moov-at-end, fragmented and robust-muxing modes */
   guint64 mdat_pos;
+  /* position of the mdat atom header of the latest fragment for writing
+   * the default base offset in fragmented mode first-moov-then-finalise and
+   * any other future non-streaming fragmented mode */
+  guint64 moof_mdat_pos;
 
   /* keep track of the largest chunk to fine-tune brands */
   GstClockTime longest_chunk;
@@ -276,7 +297,7 @@ struct _GstQTMux
   guint32 fragment_duration;
   /* Whether or not to work in 'streamable' mode and not
    * seek to rewrite headers - only valid for fragmented
-   * mode. */
+   * mode. Deprecated */
   gboolean streamable;
 
   /* Requested target maximum duration */
index 7fa741a..6ccfb20 100644 (file)
@@ -179,7 +179,7 @@ GstQTMuxFormatProp gst_qt_mux_format_list[] = {
         GST_RANK_PRIMARY,
         "qtmux",
         "QuickTime",
-        "GstQTMux",
+        "GstQTMuxElement",
         GST_STATIC_CAPS ("video/quicktime, variant = (string) apple; "
             "video/quicktime"),
         GST_STATIC_CAPS ("video/x-raw, "
index 5da17ac..77c2af9 100644 (file)
@@ -444,6 +444,158 @@ check_qtmux_pad_fragmented (GstStaticPadTemplate * srctemplate,
   buffers = NULL;
 }
 
+static void
+check_qtmux_pad_fragmented_finalise (GstStaticPadTemplate * srctemplate,
+    const gchar * sinkname, guint32 dts_method, gboolean streamable)
+{
+  GstElement *qtmux;
+  GstBuffer *inbuffer, *outbuffer;
+  GstCaps *caps;
+  int num_buffers;
+  int i;
+  guint8 data0[12] = "\000\000\000\024ftypqt  ";
+  guint8 data1[4] = "mdat";
+  guint8 data2[4] = "moov";
+  guint8 data3[4] = "moof";
+  GstSegment segment;
+
+  qtmux = setup_qtmux (srctemplate, sinkname, !streamable);
+  g_object_set (qtmux, "dts-method", dts_method, NULL);
+  g_object_set (qtmux, "fragment-duration", 40, NULL);
+  g_object_set (qtmux, "fragment-mode", 1, NULL);
+  g_object_set (qtmux, "streamable", streamable, NULL);
+  fail_unless (gst_element_set_state (qtmux,
+          GST_STATE_PLAYING) == GST_STATE_CHANGE_SUCCESS,
+      "could not set to playing");
+
+  gst_pad_push_event (mysrcpad, gst_event_new_stream_start ("test"));
+
+  caps = gst_pad_get_pad_template_caps (mysrcpad);
+  gst_pad_set_caps (mysrcpad, caps);
+  gst_caps_unref (caps);
+
+  /* ensure segment (format) properly setup */
+  gst_segment_init (&segment, GST_FORMAT_TIME);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_segment (&segment)));
+
+  inbuffer = gst_buffer_new_and_alloc (1);
+  gst_buffer_memset (inbuffer, 0, 0, 1);
+  GST_BUFFER_TIMESTAMP (inbuffer) = 0 * 40 * GST_MSECOND;
+  GST_BUFFER_DURATION (inbuffer) = 40 * GST_MSECOND;
+  ASSERT_BUFFER_REFCOUNT (inbuffer, "inbuffer", 1);
+  fail_unless (gst_pad_push (mysrcpad, inbuffer) == GST_FLOW_OK);
+
+  inbuffer = gst_buffer_new_and_alloc (1);
+  gst_buffer_memset (inbuffer, 0, 0, 1);
+  GST_BUFFER_TIMESTAMP (inbuffer) = 1 * 40 * GST_MSECOND;
+  GST_BUFFER_DURATION (inbuffer) = 40 * GST_MSECOND;
+  ASSERT_BUFFER_REFCOUNT (inbuffer, "inbuffer", 1);
+  fail_unless (gst_pad_push (mysrcpad, inbuffer) == GST_FLOW_OK);
+
+  inbuffer = gst_buffer_new_and_alloc (1);
+  gst_buffer_memset (inbuffer, 0, 0, 1);
+  GST_BUFFER_TIMESTAMP (inbuffer) = 2 * 40 * GST_MSECOND;
+  GST_BUFFER_DURATION (inbuffer) = 40 * GST_MSECOND;
+  ASSERT_BUFFER_REFCOUNT (inbuffer, "inbuffer", 1);
+  fail_unless (gst_pad_push (mysrcpad, inbuffer) == GST_FLOW_OK);
+
+  /* send eos to have all written */
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_eos ()) == TRUE);
+
+  wait_for_eos ();
+
+  num_buffers = g_list_length (buffers);
+  fail_unless (num_buffers >= 13);
+
+  /* clean up first to clear any pending refs in sticky caps */
+  cleanup_qtmux (qtmux, sinkname);
+
+  for (i = 0; i < num_buffers; ++i) {
+    outbuffer = GST_BUFFER (buffers->data);
+    fail_if (outbuffer == NULL);
+    buffers = g_list_remove (buffers, outbuffer);
+
+    switch (i) {
+      case 0:
+      {
+        /* ftyp header */
+        fail_unless (gst_buffer_get_size (outbuffer) >= 20);
+        fail_unless (gst_buffer_memcmp (outbuffer, 0, data0,
+                sizeof (data0)) == 0);
+        fail_unless (gst_buffer_memcmp (outbuffer, 16, data0 + 8, 4) == 0);
+        break;
+      }
+      case 1:                  /* first moov mdat header */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 16);
+        fail_unless (gst_buffer_memcmp (outbuffer, 12, data1,
+                sizeof (data1)) == 0);
+        break;
+      case 2:                  /* buffer we put in */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 1);
+        break;
+      case 3:                  /* first moov mdat header rewrite */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 16);
+        fail_unless (gst_buffer_memcmp (outbuffer, 12, data1,
+                sizeof (data1)) == 0);
+        break;
+      case 4:                  /* moov */
+        fail_unless (gst_buffer_get_size (outbuffer) > 8);
+        fail_unless (gst_buffer_memcmp (outbuffer, 4, data2,
+                sizeof (data2)) == 0);
+        break;
+      case 5:                  /* fragment mdat header size == 0 */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 16);
+        fail_unless (gst_buffer_memcmp (outbuffer, 12, data1,
+                sizeof (data1)) == 0);
+        break;
+      case 6:                  /* buffer we put in */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 1);
+        break;
+      case 7:                  /* fragment mdat header size */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 16);
+        fail_unless (gst_buffer_memcmp (outbuffer, 12, data1,
+                sizeof (data1)) == 0);
+        break;
+      case 8:                  /* moof */
+        fail_unless (gst_buffer_get_size (outbuffer) > 8);
+        fail_unless (gst_buffer_memcmp (outbuffer, 4, data3,
+                sizeof (data3)) == 0);
+        break;
+      case 9:                  /* fragment mdat header size = 0 */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 16);
+        fail_unless (gst_buffer_memcmp (outbuffer, 12, data1,
+                sizeof (data1)) == 0);
+        break;
+      case 10:                 /* buffer we put in */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 1);
+        break;
+      case 11:                 /* initial moov->hoov */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 1);
+        fail_unless (gst_buffer_memcmp (outbuffer, 0, "h", 1) == 0);
+        break;
+      case 12:                 /* final moov mdat header size */
+        fail_unless_equals_int (gst_buffer_get_size (outbuffer), 16);
+        fail_unless (gst_buffer_memcmp (outbuffer, 12, data1,
+                sizeof (data1)) == 0);
+        break;
+      case 13:                 /* final moov */
+        fail_unless (gst_buffer_get_size (outbuffer) > 8);
+        fail_unless (gst_buffer_memcmp (outbuffer, 4, data2,
+                sizeof (data2)) == 0);
+        break;
+      default:
+        break;
+    }
+
+    ASSERT_BUFFER_REFCOUNT (outbuffer, "outbuffer", 1);
+    gst_buffer_unref (outbuffer);
+    outbuffer = NULL;
+  }
+
+  g_list_free (buffers);
+  buffers = NULL;
+}
+
 /* dts-method dd */
 
 GST_START_TEST (test_video_pad_dd)
@@ -475,7 +627,6 @@ GST_START_TEST (test_audio_pad_frag_dd)
 
 GST_END_TEST;
 
-
 GST_START_TEST (test_video_pad_frag_dd_streamable)
 {
   check_qtmux_pad_fragmented (&srcvideotemplate, "video_%u", 0, TRUE);
@@ -483,7 +634,6 @@ GST_START_TEST (test_video_pad_frag_dd_streamable)
 
 GST_END_TEST;
 
-
 GST_START_TEST (test_audio_pad_frag_dd_streamable)
 {
   check_qtmux_pad_fragmented (&srcaudiotemplate, "audio_%u", 0, TRUE);
@@ -491,6 +641,13 @@ GST_START_TEST (test_audio_pad_frag_dd_streamable)
 
 GST_END_TEST;
 
+GST_START_TEST (test_video_pad_frag_dd_finalise)
+{
+  check_qtmux_pad_fragmented_finalise (&srcvideotemplate, "video_%u", 0, FALSE);
+}
+
+GST_END_TEST;
+
 /* dts-method reorder */
 
 GST_START_TEST (test_video_pad_reorder)
@@ -538,6 +695,13 @@ GST_START_TEST (test_audio_pad_frag_reorder_streamable)
 
 GST_END_TEST;
 
+GST_START_TEST (test_video_pad_frag_reorder_finalise)
+{
+  check_qtmux_pad_fragmented_finalise (&srcvideotemplate, "video_%u", 1, FALSE);
+}
+
+GST_END_TEST;
+
 /* dts-method asc */
 
 GST_START_TEST (test_video_pad_asc)
@@ -585,6 +749,13 @@ GST_START_TEST (test_audio_pad_frag_asc_streamable)
 
 GST_END_TEST;
 
+GST_START_TEST (test_video_pad_frag_asc_finalise)
+{
+  check_qtmux_pad_fragmented_finalise (&srcvideotemplate, "video_%u", 2, FALSE);
+}
+
+GST_END_TEST;
+
 GST_START_TEST (test_reuse)
 {
   GstElement *qtmux = setup_qtmux (&srcvideotemplate, "video_%u", TRUE);
@@ -1651,6 +1822,7 @@ qtmux_suite (void)
   tcase_add_test (tc_chain, test_audio_pad_frag_dd);
   tcase_add_test (tc_chain, test_video_pad_frag_dd_streamable);
   tcase_add_test (tc_chain, test_audio_pad_frag_dd_streamable);
+  tcase_add_test (tc_chain, test_video_pad_frag_dd_finalise);
 
   tcase_add_test (tc_chain, test_video_pad_reorder);
   tcase_add_test (tc_chain, test_audio_pad_reorder);
@@ -1658,6 +1830,7 @@ qtmux_suite (void)
   tcase_add_test (tc_chain, test_audio_pad_frag_reorder);
   tcase_add_test (tc_chain, test_video_pad_frag_reorder_streamable);
   tcase_add_test (tc_chain, test_audio_pad_frag_reorder_streamable);
+  tcase_add_test (tc_chain, test_video_pad_frag_reorder_finalise);
 
   tcase_add_test (tc_chain, test_video_pad_asc);
   tcase_add_test (tc_chain, test_audio_pad_asc);
@@ -1665,6 +1838,7 @@ qtmux_suite (void)
   tcase_add_test (tc_chain, test_audio_pad_frag_asc);
   tcase_add_test (tc_chain, test_video_pad_frag_asc_streamable);
   tcase_add_test (tc_chain, test_audio_pad_frag_asc_streamable);
+  tcase_add_test (tc_chain, test_video_pad_frag_asc_finalise);
 
   tcase_add_test (tc_chain, test_average_bitrate);