LCOV - code coverage report
Current view: top level - src/lib/post-process/mp4mx.c (source / functions) Coverage Total Hit
Test: coverage.filtered.info Lines: 89.3 % 413 369
Test Date: 2025-09-18 00:43:48 Functions: 84.6 % 13 11

            Line data    Source code
       1              : /*
       2              :  *                      GPAC - Multimedia Framework C SDK
       3              :  *
       4              :  *                      Authors: Deniz Ugur, Romain Bouqueau, Sohaib Larbi
       5              :  *                      Copyright (c) Motion Spell
       6              :  *                              All rights reserved
       7              :  *
       8              :  *  This file is part of the GPAC/GStreamer wrapper
       9              :  *
      10              :  *  This GPAC/GStreamer wrapper is free software; you can redistribute it
      11              :  *  and/or modify it under the terms of the GNU Affero General Public License
      12              :  *  as published by the Free Software Foundation; either version 3, or (at
      13              :  *  your option) any later version.
      14              :  *
      15              :  *  This GPAC/GStreamer wrapper is distributed in the hope that it will be
      16              :  *  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
      17              :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      18              :  *  GNU Affero General Public License for more details.
      19              :  *
      20              :  *  You should have received a copy of the GNU Affero General Public
      21              :  *  License along with this library; see the file LICENSE.  If not, write to
      22              :  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
      23              :  *
      24              :  */
      25              : 
      26              : #include "common.h"
      27              : #include "elements/gstgpactf.h"
      28              : #include "lib/memio.h"
      29              : #include <gpac/internal/isomedia_dev.h>
      30              : 
      31              : GST_DEBUG_CATEGORY_STATIC(gpac_mp4mx);
      32              : #define GST_CAT_DEFAULT gpac_mp4mx
      33              : 
      34              : #define GET_TYPE(type) mp4mx_ctx->contents[type]
      35              : 
      36              : static guint32 INIT_BOXES[] = { GF_ISOM_BOX_TYPE_FTYP,
      37              :                                 GF_ISOM_BOX_TYPE_FREE,
      38              :                                 GF_ISOM_BOX_TYPE_MOOV,
      39              :                                 0 };
      40              : static guint32 HEADER_BOXES[] = { GF_ISOM_BOX_TYPE_STYP,
      41              :                                   GF_ISOM_BOX_TYPE_SIDX,
      42              :                                   GF_ISOM_BOX_TYPE_MOOF,
      43              :                                   0 };
      44              : static guint32 DATA_BOXES[] = { GF_ISOM_BOX_TYPE_MDAT, 0 };
      45              : 
      46              : typedef enum _BufferType
      47              : {
      48              :   INIT = 0,
      49              :   HEADER,
      50              :   DATA,
      51              : 
      52              :   LAST
      53              : } BufferType;
      54              : 
      55              : struct BoxMapping
      56              : {
      57              :   BufferType type;
      58              :   guint32* boxes;
      59              : } box_mapping[LAST] = {
      60              :   { INIT, INIT_BOXES },
      61              :   { HEADER, HEADER_BOXES },
      62              :   { DATA, DATA_BOXES },
      63              : };
      64              : 
      65              : typedef struct
      66              : {
      67              :   GstBuffer* buffer;
      68              :   gboolean is_complete;
      69              : } BufferContents;
      70              : 
      71              : typedef struct
      72              : {
      73              :   guint32 box_type;
      74              :   guint32 box_size;
      75              :   GstBuffer* buffer;
      76              :   gboolean parsed;
      77              : } BoxInfo;
      78              : 
      79              : typedef struct
      80              : {
      81              :   guint32 track_id;
      82              :   guint32 timescale;
      83              :   gboolean defaults_present;
      84              :   guint32 default_sample_duration;
      85              :   guint32 default_sample_size;
      86              :   guint32 default_sample_flags;
      87              : } TrackInfo;
      88              : 
      89              : typedef struct
      90              : {
      91              :   gboolean is_sync;
      92              :   gssize size;
      93              :   guint64 pts;
      94              :   guint64 dts;
      95              :   guint64 duration;
      96              : } SampleInfo;
      97              : 
      98              : typedef struct
      99              : {
     100              :   // Output queue for complete fragments
     101              :   GQueue* output_queue;
     102              : 
     103              :   // State
     104              :   BufferType current_type;
     105              :   guint32 segment_count;
     106              : 
     107              :   // Box parser state
     108              :   GQueue* box_queue;
     109              : 
     110              :   // Buffer contents for the init, header, and data
     111              :   BufferContents* contents[3];
     112              : 
     113              :   // Input context
     114              :   guint64 duration;
     115              :   guint64 mp4mx_ts;
     116              :   GHashTable* tracks;
     117              :   GArray* next_samples;
     118              : } Mp4mxCtx;
     119              : 
     120              : void
     121            9 : mp4mx_ctx_init(void** process_ctx)
     122              : {
     123            9 :   *process_ctx = g_new0(Mp4mxCtx, 1);
     124            9 :   Mp4mxCtx* ctx = (Mp4mxCtx*)*process_ctx;
     125              : 
     126            9 :   GST_DEBUG_CATEGORY_INIT(
     127              :     gpac_mp4mx, "gpacmp4mxpp", 0, "GPAC mp4mx post-processor");
     128              : 
     129              :   // Initialize the context
     130            9 :   ctx->output_queue = g_queue_new();
     131            9 :   ctx->current_type = INIT;
     132            9 :   ctx->segment_count = 0;
     133            9 :   ctx->box_queue = g_queue_new();
     134              : 
     135              :   // Allocate tracks and next samples
     136            9 :   ctx->tracks =
     137            9 :     g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
     138            9 :   ctx->next_samples = g_array_new(FALSE, TRUE, sizeof(SampleInfo));
     139              : 
     140              :   // Initialize the buffer contents
     141           36 :   for (guint i = 0; i < LAST; i++) {
     142           27 :     ctx->contents[i] = g_new0(BufferContents, 1);
     143           27 :     ctx->contents[i]->is_complete = FALSE;
     144              :   }
     145            9 : }
     146              : 
     147              : void
     148            0 : mp4mx_ctx_free(void* process_ctx)
     149              : {
     150            0 :   Mp4mxCtx* ctx = (Mp4mxCtx*)process_ctx;
     151              : 
     152              :   // Free the output queue
     153            0 :   while (!g_queue_is_empty(ctx->output_queue))
     154            0 :     gst_buffer_unref((GstBuffer*)g_queue_pop_head(ctx->output_queue));
     155            0 :   g_queue_free(ctx->output_queue);
     156              : 
     157              :   // Free the box queue
     158            0 :   while (!g_queue_is_empty(ctx->box_queue)) {
     159            0 :     BoxInfo* buf = g_queue_pop_head(ctx->box_queue);
     160            0 :     if (buf->buffer)
     161            0 :       gst_buffer_unref(buf->buffer);
     162              :   }
     163            0 :   g_queue_free(ctx->box_queue);
     164              : 
     165              :   // Free the buffer contents
     166            0 :   for (guint i = 0; i < LAST; i++) {
     167            0 :     if (ctx->contents[i]->buffer)
     168            0 :       gst_buffer_unref(ctx->contents[i]->buffer);
     169            0 :     g_free(ctx->contents[i]);
     170              :   }
     171              : 
     172              :   // Free the tracks and next samples
     173            0 :   g_hash_table_destroy(ctx->tracks);
     174            0 :   g_array_free(ctx->next_samples, TRUE);
     175              : 
     176              :   // Free the context
     177            0 :   g_free(ctx);
     178            0 : }
     179              : 
     180              : GF_Err
     181            9 : mp4mx_configure_pid(GF_Filter* filter, GF_FilterPid* pid)
     182              : {
     183              :   GPAC_MemOutPIDContext* pctx =
     184            9 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     185            9 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)pctx->private_ctx;
     186              : 
     187              :   // Get the timescale from the PID
     188            9 :   mp4mx_ctx->mp4mx_ts = GST_SECOND;
     189              :   const GF_PropertyValue* p =
     190            9 :     gf_filter_pid_get_property(pid, GF_PROP_PID_TIMESCALE);
     191            9 :   if (p)
     192            9 :     mp4mx_ctx->mp4mx_ts = p->value.uint;
     193              : 
     194            9 :   return GF_OK;
     195              : }
     196              : 
     197              : Bool
     198            0 : mp4mx_process_event(GF_Filter* filter, const GF_FilterEvent* evt)
     199              : {
     200            0 :   return GF_FALSE; // No event processing
     201              : }
     202              : 
     203              : GstMemory*
     204          115 : mp4mx_create_memory(const u8* data, guint32 size, GF_FilterPacket* pck)
     205              : {
     206          115 :   gf_filter_pck_ref(&pck);
     207          115 :   return gst_memory_new_wrapped(GST_MEMORY_FLAG_READONLY,
     208              :                                 (gpointer)data,
     209              :                                 size,
     210              :                                 0,
     211              :                                 size,
     212              :                                 pck,
     213              :                                 (GDestroyNotify)gf_filter_pck_unref);
     214              : }
     215              : 
     216              : gboolean
     217          408 : mp4mx_is_box_complete(BoxInfo* box)
     218              : {
     219          816 :   return box && box->buffer &&
     220          408 :          gst_buffer_get_size(box->buffer) == box->box_size;
     221              : }
     222              : 
     223              : GF_Err
     224            9 : mp4mx_parse_moov(GF_Filter* filter, GF_FilterPid* pid, GstBuffer* buffer)
     225              : {
     226            9 :   GPAC_MemIoContext* ctx = (GPAC_MemIoContext*)gf_filter_get_rt_udta(filter);
     227              :   GPAC_MemOutPIDContext* pctx =
     228            9 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     229            9 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)pctx->private_ctx;
     230              : 
     231              :   // Map the buffer
     232            9 :   g_auto(GstBufferMapInfo) map = GST_MAP_INFO_INIT;
     233            9 :   if (G_UNLIKELY(!gst_buffer_map(buffer, &map, GST_MAP_READ))) {
     234            0 :     GST_ELEMENT_ERROR(ctx->sess->element,
     235              :                       STREAM,
     236              :                       FAILED,
     237              :                       (NULL),
     238              :                       ("Failed to map moov buffer"));
     239            0 :     return GF_CORRUPTED_DATA;
     240              :   }
     241              : 
     242              :   // Parse the box
     243            9 :   GF_BitStream* bs = gf_bs_new(map.data, map.size, GF_BITSTREAM_READ);
     244            9 :   GF_Box* moov = NULL;
     245            9 :   GF_Err err = gf_isom_box_parse(&moov, bs);
     246            9 :   if (err != GF_OK) {
     247            0 :     GST_ELEMENT_ERROR(
     248              :       ctx->sess->element, STREAM, FAILED, (NULL), ("Failed to parse moov box"));
     249            0 :     return GF_CORRUPTED_DATA;
     250              :   }
     251              : 
     252              :   // Check if we have duration in the mvhd box
     253            9 :   GF_MovieHeaderBox* mvhd = (GF_MovieHeaderBox*)gf_isom_box_find_child(
     254            9 :     moov->child_boxes, GF_ISOM_BOX_TYPE_MVHD);
     255            9 :   if (mvhd) {
     256            9 :     mp4mx_ctx->duration =
     257            9 :       gf_timestamp_rescale(mvhd->duration, mvhd->timeScale, GST_SECOND);
     258              :   }
     259              : 
     260              :   // Go through all tracks
     261            9 :   guint32 count = gf_list_count(moov->child_boxes);
     262           54 :   for (guint32 i = 0; i < count; i++) {
     263           45 :     GF_Box* box = (GF_Box*)gf_list_get(moov->child_boxes, i);
     264           45 :     if (box->type != GF_ISOM_BOX_TYPE_TRAK)
     265           35 :       continue;
     266              : 
     267              :     // Create new tracks
     268           10 :     TrackInfo* track = g_new0(TrackInfo, 1);
     269              : 
     270              :     // Get the track id
     271           10 :     GF_TrackHeaderBox* tkhd = (GF_TrackHeaderBox*)gf_isom_box_find_child(
     272              :       box->child_boxes, GF_ISOM_BOX_TYPE_TKHD);
     273           10 :     track->track_id = tkhd->trackID;
     274              : 
     275              :     // Get the timescale
     276              :     GF_Box* mdia =
     277           10 :       gf_isom_box_find_child(box->child_boxes, GF_ISOM_BOX_TYPE_MDIA);
     278           10 :     GF_MediaHeaderBox* mdhd = (GF_MediaHeaderBox*)gf_isom_box_find_child(
     279              :       mdia->child_boxes, GF_ISOM_BOX_TYPE_MDHD);
     280           10 :     track->timescale = mdhd->timeScale;
     281              : 
     282           10 :     GST_DEBUG_OBJECT(ctx->sess->element,
     283              :                      "Found track %d with timescale %d",
     284              :                      track->track_id,
     285              :                      track->timescale);
     286              : 
     287              :     // Find the mvex for this track
     288           61 :     for (guint32 j = 0; j < gf_list_count(moov->child_boxes); j++) {
     289           51 :       GF_Box* mvex = (GF_Box*)gf_list_get(moov->child_boxes, j);
     290           51 :       if (mvex->type != GF_ISOM_BOX_TYPE_MVEX)
     291           42 :         continue;
     292              : 
     293              :       // Find the track fragment box
     294           29 :       for (guint32 k = 0; k < gf_list_count(mvex->child_boxes); k++) {
     295              :         GF_TrackExtendsBox* trex =
     296           20 :           (GF_TrackExtendsBox*)gf_list_get(mvex->child_boxes, k);
     297           20 :         if (trex->type != GF_ISOM_BOX_TYPE_TREX)
     298            9 :           continue;
     299              : 
     300              :         // Check if this is the right track
     301           11 :         if (trex->trackID != track->track_id)
     302            2 :           continue;
     303              : 
     304              :         // Set the defaults
     305            9 :         track->defaults_present = TRUE;
     306            9 :         track->default_sample_duration = trex->def_sample_duration;
     307            9 :         track->default_sample_size = trex->def_sample_size;
     308            9 :         track->default_sample_flags = trex->def_sample_flags;
     309              : 
     310            9 :         GST_DEBUG_OBJECT(ctx->sess->element,
     311              :                          "Found defaults for track %d: duration: %d, size: %d, "
     312              :                          "flags: %d",
     313              :                          track->track_id,
     314              :                          track->default_sample_duration,
     315              :                          track->default_sample_size,
     316              :                          track->default_sample_flags);
     317              :       }
     318              :     }
     319              : 
     320              :     // Add the track to the hash table
     321           10 :     g_hash_table_insert(
     322           10 :       mp4mx_ctx->tracks, GUINT_TO_POINTER(track->track_id), track);
     323              :   }
     324              : 
     325              :   // Free the box
     326            9 :   gf_isom_box_del(moov);
     327            9 :   gf_bs_del(bs);
     328              : 
     329            9 :   return GF_OK;
     330              : }
     331              : 
     332              : GF_Err
     333           30 : mp4mx_parse_moof(GF_Filter* filter, GF_FilterPid* pid, GstBuffer* buffer)
     334              : {
     335           30 :   GPAC_MemIoContext* ctx = (GPAC_MemIoContext*)gf_filter_get_rt_udta(filter);
     336              :   GPAC_MemOutPIDContext* pctx =
     337           30 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     338           30 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)pctx->private_ctx;
     339              : 
     340              :   // Map the buffer
     341           30 :   g_auto(GstBufferMapInfo) map = GST_MAP_INFO_INIT;
     342           30 :   if (G_UNLIKELY(!gst_buffer_map(buffer, &map, GST_MAP_READ))) {
     343            0 :     GST_ELEMENT_ERROR(ctx->sess->element,
     344              :                       STREAM,
     345              :                       FAILED,
     346              :                       (NULL),
     347              :                       ("Failed to map moof buffer"));
     348            0 :     return GF_CORRUPTED_DATA;
     349              :   }
     350              : 
     351              :   // Parse the box
     352           30 :   GF_BitStream* bs = gf_bs_new(map.data, map.size, GF_BITSTREAM_READ);
     353           30 :   GF_Box* moof = NULL;
     354           30 :   GF_Err err = gf_isom_box_parse(&moof, bs);
     355           30 :   if (err != GF_OK) {
     356            0 :     GST_ELEMENT_ERROR(
     357              :       ctx->sess->element, STREAM, FAILED, (NULL), ("Failed to parse moof box"));
     358            0 :     return GF_CORRUPTED_DATA;
     359              :   }
     360              : 
     361              :   // Check if there are multiple tracks
     362           30 :   guint32 count = 0;
     363           89 :   for (guint32 i = 0; i < gf_list_count(moof->child_boxes); i++) {
     364           59 :     GF_Box* box = (GF_Box*)gf_list_get(moof->child_boxes, i);
     365           59 :     if (box->type == GF_ISOM_BOX_TYPE_TRAF)
     366           29 :       count++;
     367              :   }
     368           30 :   if (count > 1) {
     369            0 :     GST_FIXME_OBJECT(ctx->sess->element,
     370              :                      "Multiple tracks in moof not supported yet");
     371            0 :     return GF_NOT_SUPPORTED;
     372              :   }
     373              : 
     374              :   // Find the required boxes
     375              :   GF_Box* traf =
     376           30 :     gf_isom_box_find_child(moof->child_boxes, GF_ISOM_BOX_TYPE_TRAF);
     377           30 :   if (!traf) {
     378            1 :     GST_DEBUG_OBJECT(ctx->sess->element, "No traf box found");
     379            1 :     g_array_set_size(mp4mx_ctx->next_samples, 0);
     380            1 :     goto empty_moof;
     381              :   }
     382              : 
     383              :   GF_TrackFragmentHeaderBox* tfhd =
     384           29 :     (GF_TrackFragmentHeaderBox*)gf_isom_box_find_child(traf->child_boxes,
     385              :                                                        GF_ISOM_BOX_TYPE_TFHD);
     386              :   GF_TFBaseMediaDecodeTimeBox* tfdt =
     387           29 :     (GF_TFBaseMediaDecodeTimeBox*)gf_isom_box_find_child(traf->child_boxes,
     388              :                                                          GF_ISOM_BOX_TYPE_TFDT);
     389              :   GF_TrackFragmentRunBox* trun =
     390           29 :     (GF_TrackFragmentRunBox*)gf_isom_box_find_child(traf->child_boxes,
     391              :                                                     GF_ISOM_BOX_TYPE_TRUN);
     392              : 
     393              :   // Get the track info
     394              :   TrackInfo* track =
     395           29 :     g_hash_table_lookup(mp4mx_ctx->tracks, GUINT_TO_POINTER(tfhd->trackID));
     396           29 :   if (!track) {
     397            0 :     GST_ERROR_OBJECT(ctx->sess->element, "Track %d not found", tfhd->trackID);
     398            0 :     return GF_BAD_PARAM;
     399              :   }
     400              : 
     401              :   // Declare the defaults
     402           29 :   guint32 default_sample_duration = track->default_sample_duration;
     403           29 :   guint32 default_sample_size = track->default_sample_size;
     404           29 :   guint32 default_sample_flags = track->default_sample_flags;
     405              : 
     406              :   // Look at the track fragment header box
     407           29 :   gboolean default_sample_duration_present = (tfhd->flags & 0x8) == 0x8;
     408           29 :   gboolean default_sample_size_present = (tfhd->flags & 0x10) == 0x10;
     409           29 :   gboolean default_sample_flags_present = (tfhd->flags & 0x20) == 0x20;
     410              : 
     411           29 :   if (default_sample_duration_present)
     412           28 :     default_sample_duration = tfhd->def_sample_duration;
     413           29 :   if (default_sample_size_present)
     414            2 :     default_sample_size = tfhd->def_sample_size;
     415           29 :   if (default_sample_flags_present)
     416           19 :     default_sample_flags = tfhd->def_sample_flags;
     417              : 
     418              :   // Resize the next samples array
     419           29 :   g_array_set_size(mp4mx_ctx->next_samples, trun->sample_count);
     420              : 
     421              :   // Look at all samples
     422         1069 :   for (guint32 i = 0; i < trun->sample_count; i++) {
     423         1040 :     GF_TrunEntry* entry = &trun->samples[i];
     424         1040 :     SampleInfo* sample = &g_array_index(mp4mx_ctx->next_samples, SampleInfo, i);
     425              : 
     426              :     // Check if this is a sync sample
     427         1040 :     gboolean first_sample_flags_present = (trun->flags & 0x4) == 0x4;
     428         1040 :     gboolean sample_flags_present = (trun->flags & 0x400) == 0x400;
     429              : 
     430         1040 :     u32 flags = 0;
     431         1040 :     if (i == 0 && first_sample_flags_present) {
     432            0 :       flags = trun->first_sample_flags;
     433         1040 :     } else if (sample_flags_present) {
     434          514 :       flags = entry->flags;
     435          526 :     } else if (default_sample_flags_present || track->defaults_present) {
     436          526 :       flags = default_sample_flags;
     437              :     } else {
     438            0 :       GST_ERROR_OBJECT(ctx->sess->element, "No sample flags found");
     439            0 :       return GF_CORRUPTED_DATA;
     440              :     }
     441              : 
     442         1040 :     sample->is_sync = GF_ISOM_GET_FRAG_SYNC(flags);
     443              : 
     444              :     // Retrieve the sample size and offset
     445         1040 :     gboolean sample_size_present = (trun->flags & 0x200) == 0x200;
     446              : 
     447         1040 :     if (sample_size_present) {
     448         1038 :       sample->size = entry->size;
     449            2 :     } else if (default_sample_size_present || track->defaults_present) {
     450            2 :       sample->size = default_sample_size;
     451              :     } else {
     452            0 :       GST_ERROR_OBJECT(ctx->sess->element, "No sample size found");
     453            0 :       return GF_CORRUPTED_DATA;
     454              :     }
     455              : 
     456              :     // Retrieve the sample duration
     457         1040 :     gboolean sample_duration_present = (trun->flags & 0x100) == 0x100;
     458              : 
     459         1040 :     guint64 duration = 0;
     460         1040 :     if (sample_duration_present) {
     461           44 :       duration = entry->Duration;
     462          996 :     } else if (default_sample_duration_present || track->defaults_present) {
     463          996 :       duration = default_sample_duration;
     464              :     } else {
     465            0 :       GST_ERROR_OBJECT(ctx->sess->element, "No sample duration found");
     466            0 :       return GF_CORRUPTED_DATA;
     467              :     }
     468              : 
     469         1040 :     sample->duration =
     470         1040 :       gf_timestamp_rescale(duration, track->timescale, GST_SECOND);
     471              : 
     472              :     // Retrieve the sample DTS
     473         1040 :     guint64 dts = tfdt->baseMediaDecodeTime + i;
     474         1040 :     sample->dts = gf_timestamp_rescale(dts, track->timescale, GST_SECOND);
     475         1040 :     sample->dts += ctx->global_offset;
     476              : 
     477              :     // Retrieve the sample PTS
     478         1040 :     guint64 pts = dts + entry->CTS_Offset;
     479         1040 :     sample->pts = gf_timestamp_rescale(pts, track->timescale, GST_SECOND);
     480         1040 :     sample->pts += ctx->global_offset;
     481              : 
     482         1040 :     GST_TRACE_OBJECT(ctx->sess->element,
     483              :                      "Sample %d [%s]: size: %" G_GSSIZE_FORMAT ", "
     484              :                      "duration: %" GST_TIME_FORMAT ", "
     485              :                      "DTS: %" GST_TIME_FORMAT ", "
     486              :                      "PTS: %" GST_TIME_FORMAT,
     487              :                      i,
     488              :                      sample->is_sync ? "S" : "NS",
     489              :                      sample->size,
     490              :                      GST_TIME_ARGS(sample->duration),
     491              :                      GST_TIME_ARGS(sample->dts),
     492              :                      GST_TIME_ARGS(sample->pts));
     493              :   }
     494              : 
     495           29 : empty_moof:
     496              :   // Free the box
     497           30 :   gf_isom_box_del(moof);
     498           30 :   gf_bs_del(bs);
     499              : 
     500           30 :   return GF_OK;
     501              : }
     502              : 
     503              : gboolean
     504           97 : mp4mx_parse_boxes(GF_Filter* filter, GF_FilterPid* pid, GF_FilterPacket* pck)
     505              : {
     506           97 :   GPAC_MemIoContext* ctx = (GPAC_MemIoContext*)gf_filter_get_rt_udta(filter);
     507              :   GPAC_MemOutPIDContext* pctx =
     508           97 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     509           97 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)pctx->private_ctx;
     510              : 
     511              :   // Get the data
     512              :   u32 size;
     513           97 :   const u8* data = gf_filter_pck_get_data(pck, &size);
     514           97 :   GST_DEBUG_OBJECT(
     515              :     ctx->sess->element, "Processing data of size %" G_GUINT32_FORMAT, size);
     516              : 
     517           97 :   guint32 offset = 0;
     518          300 :   while (offset < size) {
     519          203 :     BoxInfo* box = g_queue_peek_tail(mp4mx_ctx->box_queue);
     520          203 :     if (!box || mp4mx_is_box_complete(box)) {
     521           88 :       box = g_new0(BoxInfo, 1);
     522           88 :       g_queue_push_tail(mp4mx_ctx->box_queue, box);
     523              :     }
     524              : 
     525              :     // If we have leftover data, append it to the current buffer
     526          203 :     if (box->buffer && gst_buffer_get_size(box->buffer) != box->box_size) {
     527          345 :       guint32 leftover =
     528          115 :         MIN(box->box_size - gst_buffer_get_size(box->buffer), size - offset);
     529          115 :       if (gst_buffer_get_size(box->buffer) > 0)
     530           27 :         GST_DEBUG_OBJECT(ctx->sess->element,
     531              :                          "Incomplete box %s, appending %" G_GUINT32_FORMAT
     532              :                          " bytes",
     533              :                          gf_4cc_to_str(box->box_type),
     534              :                          leftover);
     535          115 :       GstMemory* mem = mp4mx_create_memory(data + offset, leftover, pck);
     536              : 
     537              :       // Append the memory to the buffer
     538          115 :       gst_buffer_append_memory(box->buffer, mem);
     539              : 
     540              :       // Update the offset
     541          115 :       offset += leftover;
     542          115 :       GST_DEBUG_OBJECT(ctx->sess->element,
     543              :                        "Wrote %" G_GSIZE_FORMAT " bytes of %" G_GUINT32_FORMAT
     544              :                        " for box %s",
     545              :                        gst_buffer_get_size(box->buffer),
     546              :                        box->box_size,
     547              :                        gf_4cc_to_str(box->box_type));
     548          115 :       continue;
     549              :     }
     550              : 
     551              :     // Parse the box header
     552           88 :     box->box_size =
     553           88 :       GUINT32_FROM_LE((data[offset] << 24) | (data[offset + 1] << 16) |
     554              :                       (data[offset + 2] << 8) | data[offset + 3]);
     555           88 :     box->box_type =
     556           88 :       GUINT32_FROM_LE((data[offset + 4] << 24) | (data[offset + 5] << 16) |
     557              :                       (data[offset + 6] << 8) | data[offset + 7]);
     558           88 :     GST_DEBUG_OBJECT(ctx->sess->element,
     559              :                      "Saw box %s with size %" G_GUINT32_FORMAT,
     560              :                      gf_4cc_to_str(box->box_type),
     561              :                      box->box_size);
     562              : 
     563              :     // Create a new buffer
     564           88 :     box->buffer = gst_buffer_new();
     565              : 
     566              :     // Preserve the timing information
     567           88 :     GST_BUFFER_PTS(box->buffer) = gf_filter_pck_get_cts(pck);
     568           88 :     GST_BUFFER_DTS(box->buffer) = gf_filter_pck_get_dts(pck);
     569           88 :     GST_BUFFER_DURATION(box->buffer) = gf_filter_pck_get_duration(pck);
     570              :   }
     571              : 
     572              :   // Check if process can continue
     573           97 :   BoxInfo* box = g_queue_peek_head(mp4mx_ctx->box_queue);
     574           97 :   if (!mp4mx_is_box_complete(box))
     575           26 :     return FALSE;
     576              : 
     577              :   // Check if we need any further parsing
     578          160 :   for (guint i = 0; i < g_queue_get_length(mp4mx_ctx->box_queue); i++) {
     579           89 :     BoxInfo* box = g_queue_peek_nth(mp4mx_ctx->box_queue, i);
     580           89 :     if (!mp4mx_is_box_complete(box) || box->parsed)
     581            1 :       continue;
     582              : 
     583           88 :     switch (box->box_type) {
     584            9 :       case GF_ISOM_BOX_TYPE_MOOV:
     585            9 :         gpac_return_val_if_fail(mp4mx_parse_moov(filter, pid, box->buffer),
     586              :                                 FALSE);
     587            9 :         break;
     588              : 
     589           30 :       case GF_ISOM_BOX_TYPE_MOOF:
     590           30 :         gpac_return_val_if_fail(mp4mx_parse_moof(filter, pid, box->buffer),
     591              :                                 FALSE);
     592           30 :         break;
     593              : 
     594           49 :       default:
     595           49 :         break;
     596              :     }
     597              : 
     598           88 :     box->parsed = TRUE;
     599              :   }
     600              : 
     601           71 :   return TRUE;
     602              : }
     603              : 
     604              : GstBufferList*
     605           31 : mp4mx_create_buffer_list(GF_Filter* filter, GF_FilterPid* pid)
     606              : {
     607           31 :   GPAC_MemIoContext* ctx = (GPAC_MemIoContext*)gf_filter_get_rt_udta(filter);
     608           31 :   GstGpacTransform* gpac_tf = GST_GPAC_TF(GST_ELEMENT(ctx->sess->element));
     609              :   GPAC_MemOutPIDContext* pctx =
     610           31 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     611           31 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)pctx->private_ctx;
     612              : 
     613              :   // Check if the init and header buffers are present
     614           31 :   gboolean init_present = GET_TYPE(INIT)->is_complete && GET_TYPE(INIT)->buffer;
     615           31 :   gboolean header_present =
     616           31 :     GET_TYPE(HEADER)->is_complete && GET_TYPE(HEADER)->buffer;
     617              : 
     618              :   // Declare variables
     619           31 :   GstMemory* mdat_hdr = NULL;
     620           31 :   gboolean segment_boundary = FALSE;
     621              : 
     622              :   // Create a new buffer list
     623           31 :   GstBufferList* buffer_list = gst_buffer_list_new();
     624           31 :   GST_DEBUG_OBJECT(ctx->sess->element, "Fragment completed");
     625              : 
     626              :   //
     627              :   // Split the DATA buffer into samples, if we have the sample information
     628              :   //
     629              : 
     630              :   // Copy the data as is if we don't have sample information
     631           31 :   gboolean has_sample_info = mp4mx_ctx->next_samples->len > 0;
     632           31 :   if (!has_sample_info) {
     633            2 :     GST_DEBUG_OBJECT(
     634              :       ctx->sess->element,
     635              :       "No sample information found, appending data buffer as is");
     636              : 
     637              :     // Set the flags
     638            2 :     GST_BUFFER_FLAG_SET(GET_TYPE(DATA)->buffer, GST_BUFFER_FLAG_MARKER);
     639            2 :     GST_BUFFER_FLAG_SET(GET_TYPE(DATA)->buffer, GST_BUFFER_FLAG_DELTA_UNIT);
     640              : 
     641              :     // size of the data buffer
     642            2 :     guint64 size = gst_buffer_get_size(GET_TYPE(DATA)->buffer);
     643              : 
     644              :     // We have to rely on mp4mx timing information
     645              : 
     646              :     // PTS
     647            2 :     guint64 pts = gf_timestamp_rescale(
     648            2 :       GST_BUFFER_PTS(GET_TYPE(DATA)->buffer), mp4mx_ctx->mp4mx_ts, GST_SECOND);
     649            2 :     pts += ctx->global_offset;
     650            2 :     GST_BUFFER_PTS(GET_TYPE(DATA)->buffer) = pts;
     651              : 
     652              :     // DTS
     653            2 :     guint64 dts = gf_timestamp_rescale(
     654            2 :       GST_BUFFER_DTS(GET_TYPE(DATA)->buffer), mp4mx_ctx->mp4mx_ts, GST_SECOND);
     655            2 :     dts += ctx->global_offset;
     656            2 :     GST_BUFFER_DTS(GET_TYPE(DATA)->buffer) = dts;
     657              : 
     658              :     // Duration
     659              :     // For duration, we have to use mvhd since we don't parse the moov for
     660              :     // sample informations
     661            2 :     GST_BUFFER_DURATION(GET_TYPE(DATA)->buffer) = mp4mx_ctx->duration;
     662              : 
     663            2 :     gst_buffer_list_add(buffer_list, GET_TYPE(DATA)->buffer);
     664            2 :     goto headers;
     665              :   }
     666              : 
     667              :   // Move the mdat header out of the data buffer
     668              :   mdat_hdr =
     669           29 :     gst_memory_share(gst_buffer_peek_memory(GET_TYPE(DATA)->buffer, 0), 0, 8);
     670           29 :   gst_buffer_resize(GET_TYPE(DATA)->buffer, 8, -1);
     671              : 
     672              :   // Go through all samples
     673           29 :   gssize data_offset = 0;
     674         1069 :   for (guint s = 0; s < mp4mx_ctx->next_samples->len; s++) {
     675         1040 :     SampleInfo* sample = &g_array_index(mp4mx_ctx->next_samples, SampleInfo, s);
     676              : 
     677              :     // Create a new buffer
     678         1040 :     GstBuffer* sample_buffer = gst_buffer_new();
     679              : 
     680              :     // Slice the buffer
     681         1040 :     gssize avail_sample_size = sample->size;
     682         1040 :     gssize offset = 0;
     683         1040 :     guint n_blocks = gst_buffer_n_memory(GET_TYPE(DATA)->buffer);
     684         2080 :     for (guint b = 0; b < n_blocks && avail_sample_size; b++) {
     685         1040 :       GstMemory* mem = gst_buffer_peek_memory(GET_TYPE(DATA)->buffer, b);
     686         1040 :       gssize size = (gssize)gst_memory_get_sizes(mem, NULL, NULL);
     687              : 
     688              :       // Skip until we reach the data offset
     689         1040 :       if (offset + size <= data_offset)
     690            0 :         goto skip;
     691              : 
     692              :       // Check the offset within the block
     693         1040 :       gssize block_offset = 0;
     694         1040 :       if (offset < data_offset) {
     695         1011 :         block_offset = data_offset - offset;
     696         1011 :         offset += block_offset;
     697         1011 :         size -= block_offset;
     698              :       }
     699              : 
     700              :       // Check if the block is enough
     701         1040 :       if (size <= avail_sample_size) {
     702           29 :         gst_buffer_append_memory(sample_buffer,
     703              :                                  gst_memory_share(mem, block_offset, size));
     704           29 :         avail_sample_size -= size;
     705              :       } else {
     706         1011 :         gst_buffer_append_memory(
     707              :           sample_buffer,
     708              :           gst_memory_share(mem, block_offset, avail_sample_size));
     709         1011 :         avail_sample_size = 0;
     710              :       }
     711              : 
     712         1040 :     skip:
     713              :       // Update the local offset
     714         1040 :       offset += size;
     715              :     }
     716              : 
     717              :     // Set the marker flag if it's the last sample
     718         1040 :     if (s == mp4mx_ctx->next_samples->len - 1)
     719           29 :       GST_BUFFER_FLAG_SET(sample_buffer, GST_BUFFER_FLAG_MARKER);
     720              : 
     721              :     // Set the delta unit flag. These buffers are always delta because they
     722              :     // follow a moof
     723         1040 :     GST_BUFFER_FLAG_SET(sample_buffer, GST_BUFFER_FLAG_DELTA_UNIT);
     724         1040 :     if (sample->is_sync)
     725           55 :       segment_boundary = TRUE;
     726              : 
     727              :     // Set the PTS, DTS, and duration
     728         1040 :     GST_BUFFER_PTS(sample_buffer) = sample->pts;
     729         1040 :     GST_BUFFER_DTS(sample_buffer) = sample->dts;
     730         1040 :     GST_BUFFER_DURATION(sample_buffer) = sample->duration;
     731              : 
     732              :     // Append the sample buffer
     733         1040 :     gst_buffer_list_add(buffer_list, sample_buffer);
     734              : 
     735         1040 :     GST_TRACE_OBJECT(
     736              :       ctx->sess->element,
     737              :       "Added sample %d to the buffer list: size: %" G_GSSIZE_FORMAT ", "
     738              :       "duration: %" GST_TIME_FORMAT ", "
     739              :       "DTS: %" GST_TIME_FORMAT ", "
     740              :       "PTS: %" GST_TIME_FORMAT,
     741              :       s,
     742              :       sample->size,
     743              :       GST_TIME_ARGS(sample->duration),
     744              :       GST_TIME_ARGS(sample->dts),
     745              :       GST_TIME_ARGS(sample->pts));
     746              : 
     747              :     // Update the data offset
     748         1040 :     data_offset += sample->size;
     749              :   }
     750              : 
     751              :   // Unref the data buffer
     752           29 :   gst_buffer_unref(GET_TYPE(DATA)->buffer);
     753              : 
     754           31 : headers:
     755              :   // Add the init buffer if it's present
     756           31 :   if (init_present) {
     757            9 :     GST_DEBUG_OBJECT(ctx->sess->element, "Adding init buffer to the beginning");
     758              : 
     759              :     // Set the flags
     760            9 :     GST_BUFFER_FLAG_SET(GET_TYPE(INIT)->buffer, GST_BUFFER_FLAG_HEADER);
     761            9 :     if (mp4mx_ctx->segment_count == 0) {
     762            9 :       GST_BUFFER_FLAG_SET(GET_TYPE(INIT)->buffer, GST_BUFFER_FLAG_DISCONT);
     763            9 :       ctx->is_continuous = TRUE;
     764              :     }
     765              : 
     766              :     // Set the timing information
     767            9 :     guint64 pts = G_MAXUINT64;
     768            9 :     guint64 dts = G_MAXUINT64;
     769            9 :     if (has_sample_info) {
     770              :       // Set PTS and DTS to the minimum of the samples
     771           16 :       for (guint s = 0; s < mp4mx_ctx->next_samples->len; s++) {
     772           16 :         SampleInfo* sample =
     773           16 :           &g_array_index(mp4mx_ctx->next_samples, SampleInfo, s);
     774              : 
     775           16 :         dts = MIN(dts, sample->dts);
     776           16 :         if (sample->pts < pts)
     777            8 :           pts = sample->pts;
     778              :         else {
     779              :           // PTS can only decrease if there are B-frames, so we break
     780            8 :           break;
     781              :         }
     782              :       }
     783              :     } else {
     784              :       // Set the PTS and DTS to the minimum of the data
     785            1 :       pts = GST_BUFFER_PTS(GET_TYPE(DATA)->buffer);
     786            1 :       dts = GST_BUFFER_DTS(GET_TYPE(DATA)->buffer);
     787              :     }
     788              : 
     789            9 :     GST_BUFFER_PTS(GET_TYPE(INIT)->buffer) = pts;
     790            9 :     GST_BUFFER_DTS(GET_TYPE(INIT)->buffer) = dts;
     791            9 :     GST_BUFFER_DURATION(GET_TYPE(INIT)->buffer) = GST_CLOCK_TIME_NONE;
     792              : 
     793            9 :     gst_buffer_list_insert(buffer_list, 0, GET_TYPE(INIT)->buffer);
     794              :   }
     795              : 
     796              :   // Add the header buffer if it's present
     797           31 :   if (header_present) {
     798           30 :     GST_DEBUG_OBJECT(ctx->sess->element, "Adding header buffer after init");
     799              : 
     800              :     // Set the flags
     801           30 :     GST_BUFFER_FLAG_SET(GET_TYPE(HEADER)->buffer, GST_BUFFER_FLAG_HEADER);
     802              : 
     803              :     // Set the delta unit based on the first sample
     804           30 :     if (!segment_boundary)
     805           18 :       GST_BUFFER_FLAG_SET(GET_TYPE(HEADER)->buffer, GST_BUFFER_FLAG_DELTA_UNIT);
     806              : 
     807              :     // Set the timing information
     808              :     // Set the timing information
     809           30 :     guint64 pts = G_MAXUINT64;
     810           30 :     guint64 dts = G_MAXUINT64;
     811           30 :     guint64 duration = 0;
     812           30 :     if (has_sample_info) {
     813              :       // Set PTS and DTS to the minimum of the samples
     814         1069 :       for (guint s = 0; s < mp4mx_ctx->next_samples->len; s++) {
     815         1040 :         SampleInfo* sample =
     816         1040 :           &g_array_index(mp4mx_ctx->next_samples, SampleInfo, s);
     817              : 
     818         1040 :         pts = MIN(pts, sample->pts);
     819         1040 :         dts = MIN(dts, sample->dts);
     820         1040 :         duration += sample->duration;
     821              :       }
     822              :     } else {
     823              :       // Set the PTS and DTS to the minimum of the data
     824            1 :       pts = GST_BUFFER_PTS(GET_TYPE(DATA)->buffer);
     825            1 :       dts = GST_BUFFER_DTS(GET_TYPE(DATA)->buffer);
     826            1 :       duration = GST_BUFFER_DURATION(GET_TYPE(DATA)->buffer);
     827              :     }
     828              : 
     829           30 :     GST_BUFFER_PTS(GET_TYPE(HEADER)->buffer) = pts;
     830           30 :     GST_BUFFER_DTS(GET_TYPE(HEADER)->buffer) = dts;
     831           30 :     GST_BUFFER_DURATION(GET_TYPE(HEADER)->buffer) = duration;
     832              : 
     833              :     // Append the mdat header
     834           30 :     if (mdat_hdr)
     835           29 :       gst_buffer_append_memory(GET_TYPE(HEADER)->buffer, mdat_hdr);
     836              : 
     837              :     // Insert the header buffer
     838           30 :     gst_buffer_list_insert(
     839           30 :       buffer_list, init_present ? 1 : 0, GET_TYPE(HEADER)->buffer);
     840              :   }
     841              : 
     842              :   // Reset the buffer contents
     843          124 :   for (guint i = 0; i < LAST; i++) {
     844           93 :     GET_TYPE(i)->buffer = NULL;
     845           93 :     GET_TYPE(i)->is_complete = FALSE;
     846              :   }
     847              : 
     848           31 :   return buffer_list;
     849              : }
     850              : 
     851              : BufferType
     852           88 : mp4mx_get_buffer_type(GF_FilterPid* pid, guint32 box_type)
     853              : {
     854              :   GPAC_MemOutPIDContext* ctx =
     855           88 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     856           88 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)ctx->private_ctx;
     857              : 
     858              :   // Check if the box type is related to the current type
     859          150 :   for (guint i = mp4mx_ctx->current_type; i < LAST; i++) {
     860          423 :     for (guint j = 0; box_mapping[i].boxes[j]; j++) {
     861          361 :       if (box_mapping[i].boxes[j] == box_type)
     862           88 :         return box_mapping[i].type;
     863              :     }
     864              :   }
     865              : 
     866            0 :   return LAST;
     867              : }
     868              : 
     869              : GF_Err
     870          134 : mp4mx_post_process(GF_Filter* filter, GF_FilterPid* pid, GF_FilterPacket* pck)
     871              : {
     872          134 :   GPAC_MemIoContext* ctx = (GPAC_MemIoContext*)gf_filter_get_rt_udta(filter);
     873              :   GPAC_MemOutPIDContext* pctx =
     874          134 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     875          134 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)pctx->private_ctx;
     876          134 :   if (!pck)
     877           37 :     return GF_OK;
     878              : 
     879              :   // Parse the boxes
     880           97 :   if (!mp4mx_parse_boxes(filter, pid, pck))
     881           26 :     return GF_OK;
     882              : 
     883              :   // Iterate over the boxes
     884          159 :   while (!g_queue_is_empty(mp4mx_ctx->box_queue)) {
     885           89 :     BoxInfo* box = g_queue_peek_head(mp4mx_ctx->box_queue);
     886           89 :     if (!mp4mx_is_box_complete(box))
     887            1 :       break;
     888              : 
     889           88 :     GST_DEBUG_OBJECT(
     890              :       ctx->sess->element, "Current type: %d", mp4mx_ctx->current_type);
     891              : 
     892              :     // Get the buffer type
     893           88 :     BufferType type = mp4mx_get_buffer_type(pid, box->box_type);
     894           88 :     if (type == LAST) {
     895            0 :       GST_WARNING_OBJECT(ctx->sess->element,
     896              :                          "Box %s is not related to any current or future "
     897              :                          "buffer types",
     898              :                          gf_4cc_to_str(box->box_type));
     899            0 :       goto skip;
     900              :     }
     901              : 
     902           88 :     GST_DEBUG_OBJECT(ctx->sess->element,
     903              :                      "Found box type %s for type %d",
     904              :                      gf_4cc_to_str(box->box_type),
     905              :                      type);
     906              : 
     907              :     // Mark all previous types as complete
     908          150 :     for (guint j = mp4mx_ctx->current_type; j < type; j++)
     909           62 :       GET_TYPE(j)->is_complete = TRUE;
     910              : 
     911              :     // Mark the current type as complete if it's DATA
     912           88 :     if (type == DATA)
     913           31 :       GET_TYPE(type)->is_complete = TRUE;
     914              : 
     915              :     // Set the current type
     916           88 :     mp4mx_ctx->current_type = type;
     917              : 
     918              :     // Create the master buffer
     919           88 :     GstBuffer** master_buffer = &GET_TYPE(type)->buffer;
     920           88 :     if (!(*master_buffer))
     921           71 :       *master_buffer = box->buffer;
     922              :     else
     923           17 :       *master_buffer = gst_buffer_append(*master_buffer, box->buffer);
     924              : 
     925           88 :     GST_DEBUG_OBJECT(ctx->sess->element,
     926              :                      "New buffer [type: %d, size: %" G_GUINT32_FORMAT
     927              :                      "]: %p (PTS: %" G_GUINT64_FORMAT
     928              :                      ", DTS: %" G_GUINT64_FORMAT
     929              :                      ", duration: %" G_GUINT64_FORMAT ")",
     930              :                      type,
     931              :                      box->box_size,
     932              :                      *master_buffer,
     933              :                      GST_BUFFER_PTS(*master_buffer),
     934              :                      GST_BUFFER_DTS(*master_buffer),
     935              :                      GST_BUFFER_DURATION(*master_buffer));
     936              : 
     937           88 :   skip:
     938              :     // Pop the box
     939           88 :     g_free(g_queue_pop_head(mp4mx_ctx->box_queue));
     940              :   }
     941              : 
     942              :   // Check if the fragment is completed
     943           71 :   if (!GET_TYPE(HEADER)->is_complete || !GET_TYPE(DATA)->is_complete)
     944           40 :     return GF_OK;
     945              : 
     946              :   // Create and enqueue the buffer list
     947           31 :   GstBufferList* buffer_list = mp4mx_create_buffer_list(filter, pid);
     948           31 :   g_queue_push_tail(mp4mx_ctx->output_queue, buffer_list);
     949              : 
     950              :   // Increment the segment count
     951           31 :   mp4mx_ctx->segment_count++;
     952           31 :   GST_DEBUG_OBJECT(ctx->sess->element,
     953              :                    "Enqueued fragment #%" G_GUINT32_FORMAT,
     954              :                    mp4mx_ctx->segment_count);
     955              : 
     956              :   // Reset the current type
     957           31 :   mp4mx_ctx->current_type = INIT;
     958              : 
     959           31 :   return GF_OK;
     960              : }
     961              : 
     962              : GPAC_FilterPPRet
     963          918 : mp4mx_consume(GF_Filter* filter, GF_FilterPid* pid, void** outptr)
     964              : {
     965              :   GPAC_MemOutPIDContext* ctx =
     966          918 :     (GPAC_MemOutPIDContext*)gf_filter_pid_get_udta(pid);
     967          918 :   Mp4mxCtx* mp4mx_ctx = (Mp4mxCtx*)ctx->private_ctx;
     968              : 
     969              :   // Check if the queue is empty
     970          918 :   if (g_queue_is_empty(mp4mx_ctx->output_queue))
     971          888 :     return GPAC_FILTER_PP_RET_EMPTY;
     972              : 
     973              :   // Assign the output
     974           30 :   if (outptr) {
     975           30 :     *outptr = g_queue_pop_head(mp4mx_ctx->output_queue);
     976           30 :     return GPAC_FILTER_PP_RET_BUFFER_LIST;
     977              :   }
     978            0 :   return GPAC_FILTER_PP_RET_NULL;
     979              : }
        

Generated by: LCOV version 2.0-1