[vlc-commits] [Git][videolan/vlc][master] 4 commits: transcode: add the pcr_sync utility

Jean-Baptiste Kempf (@jbk) gitlab at videolan.org
Thu Jul 14 12:26:37 UTC 2022



Jean-Baptiste Kempf pushed to branch master at VideoLAN / VLC


Commits:
0b3aa33b by Alaric Senat at 2022-07-14T12:11:54+00:00
transcode: add the pcr_sync utility

This utility helps handling PCR events in modules where frames are
treated asynchronously. This tool basically bufferize PCR events until
all associated frames are out of the async unit.

The helper was made to handle any async frame treatment unit and could
be used in any other modules that have the same behavior. For now, only
transcode is in that case so the files will reside in the transcode
folder.

- - - - -
3712ecff by Alaric Senat at 2022-07-14T12:11:54+00:00
transcode: pcr_sync: add unit testing

- - - - -
12917f6b by Alaric Senat at 2022-07-14T12:11:54+00:00
transcode: add the pcr_helper utility

This utility handles synthetization of the PCR whenever the transcode
pipeline (any of the filter or the encoder) alter the frame timestamp
output or even, discard some blocks.

- - - - -
57ce124f by Alaric Senat at 2022-07-14T12:11:54+00:00
transcode: implement SetPCR

Refs #27050

- - - - -


9 changed files:

- modules/stream_out/Makefile.am
- + modules/stream_out/transcode/pcr_helper.c
- + modules/stream_out/transcode/pcr_helper.h
- + modules/stream_out/transcode/pcr_sync.c
- + modules/stream_out/transcode/pcr_sync.h
- modules/stream_out/transcode/transcode.c
- modules/stream_out/transcode/transcode.h
- test/Makefile.am
- + test/modules/stream_out/pcr_sync.c


Changes:

=====================================
modules/stream_out/Makefile.am
=====================================
@@ -25,7 +25,9 @@ libstream_out_transcode_plugin_la_SOURCES = \
         stream_out/transcode/encoder/spu.c \
         stream_out/transcode/encoder/video.c \
 	stream_out/transcode/spu.c \
-	stream_out/transcode/audio.c stream_out/transcode/video.c
+	stream_out/transcode/audio.c stream_out/transcode/video.c \
+	stream_out/transcode/pcr_sync.h stream_out/transcode/pcr_sync.c \
+	stream_out/transcode/pcr_helper.h stream_out/transcode/pcr_helper.c
 libstream_out_transcode_plugin_la_LIBADD = $(LIBM)
 libstream_out_udp_plugin_la_SOURCES = \
 	stream_out/sdp_helper.c stream_out/sdp_helper.h \


=====================================
modules/stream_out/transcode/pcr_helper.c
=====================================
@@ -0,0 +1,171 @@
+/*****************************************************************************
+ * pcr_helper.c:
+ *****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <assert.h>
+#include <stdint.h>
+
+#include "pcr_helper.h"
+
+#include <vlc_list.h>
+#include <vlc_tick.h>
+
+struct transcode_track_pcr_helper
+{
+    vlc_tick_t max_delay;
+    vlc_tick_t current_media_time;
+    vlc_tick_t last_dts_output;
+
+    struct vlc_list delayed_frames_data;
+
+    vlc_pcr_sync_t *sync_ref;
+    unsigned int pcr_sync_es_id;
+};
+
+typedef struct
+{
+    vlc_tick_t length;
+    vlc_tick_t dts;
+    struct vlc_list node;
+} delayed_frame_data_t;
+
+transcode_track_pcr_helper_t *transcode_track_pcr_helper_New(vlc_pcr_sync_t *sync_ref,
+                                                             vlc_tick_t max_delay)
+{
+    transcode_track_pcr_helper_t *ret = malloc(sizeof(*ret));
+    if (unlikely(ret == NULL))
+        return NULL;
+
+    if (vlc_pcr_sync_NewESID(sync_ref, &ret->pcr_sync_es_id) != VLC_SUCCESS)
+    {
+        free(ret);
+        return NULL;
+    }
+
+    ret->max_delay = max_delay;
+    ret->current_media_time = 0;
+    ret->last_dts_output = VLC_TICK_INVALID;
+    vlc_list_init(&ret->delayed_frames_data);
+    ret->sync_ref = sync_ref;
+
+    return ret;
+}
+
+void transcode_track_pcr_helper_Delete(transcode_track_pcr_helper_t *pcr_helper)
+{
+    delayed_frame_data_t *it;
+    vlc_list_foreach(it, &pcr_helper->delayed_frames_data, node)
+    {
+        vlc_list_remove(&it->node);
+        free(it);
+    }
+
+    vlc_pcr_sync_DelESID(pcr_helper->sync_ref, pcr_helper->pcr_sync_es_id);
+
+    free(pcr_helper);
+}
+
+static inline vlc_tick_t
+transcode_track_pcr_helper_GetFramePCR(transcode_track_pcr_helper_t *pcr_helper,
+                                       vlc_tick_t frame_dts)
+{
+    vlc_tick_t pcr = VLC_TICK_INVALID;
+    vlc_tick_t it = VLC_TICK_INVALID;
+
+    // XXX: `vlc_pcr_sync_SignalFrameOutput` only needs DTS for now. Passing a frame by only filling
+    // the DTS is enough.
+    const vlc_frame_t fake_frame = {.i_dts = frame_dts};
+
+    while ((it = vlc_pcr_sync_SignalFrameOutput(pcr_helper->sync_ref, pcr_helper->pcr_sync_es_id,
+                                                &fake_frame)) != VLC_TICK_INVALID)
+    {
+        pcr = it;
+    }
+    return pcr;
+}
+
+int transcode_track_pcr_helper_SignalEnteringFrame(transcode_track_pcr_helper_t *pcr_helper,
+                                                   const vlc_frame_t *frame,
+                                                   vlc_tick_t *dropped_frame_ts)
+{
+    delayed_frame_data_t *bdata = malloc(sizeof(*bdata));
+    if (unlikely(bdata == NULL))
+        return VLC_ENOMEM;
+
+    bdata->length = frame->i_length;
+    bdata->dts = frame->i_dts;
+
+    pcr_helper->current_media_time += bdata->length;
+
+    vlc_pcr_sync_SignalFrame(pcr_helper->sync_ref, pcr_helper->pcr_sync_es_id, frame);
+
+    vlc_list_append(&bdata->node, &pcr_helper->delayed_frames_data);
+
+    // Something went wrong in the delaying unit.
+    // Exceeding this limit usually means the frame was dropped. So in our case, act like it went
+    // through.
+    // TODO needs to be properly unit-tested.
+    if (pcr_helper->current_media_time > pcr_helper->max_delay)
+    {
+        delayed_frame_data_t *first_bdata = vlc_list_first_entry_or_null(
+            &pcr_helper->delayed_frames_data, delayed_frame_data_t, node);
+        assert(first_bdata != NULL);
+
+        const vlc_tick_t pcr = transcode_track_pcr_helper_GetFramePCR(pcr_helper, first_bdata->dts);
+        *dropped_frame_ts = pcr_helper->last_dts_output == VLC_TICK_INVALID
+                                ? pcr
+                                : __MIN(pcr_helper->last_dts_output, first_bdata->dts);
+
+        pcr_helper->current_media_time -= first_bdata->length;
+
+        vlc_list_remove(&first_bdata->node);
+        free(first_bdata);
+    }
+    else
+    {
+        *dropped_frame_ts = VLC_TICK_INVALID;
+    }
+    return VLC_SUCCESS;
+}
+
+vlc_tick_t transcode_track_pcr_helper_SignalLeavingFrame(transcode_track_pcr_helper_t *pcr_helper,
+                                                         const vlc_frame_t *frame)
+{
+    delayed_frame_data_t *frame_data =
+        vlc_list_first_entry_or_null(&pcr_helper->delayed_frames_data, delayed_frame_data_t, node);
+
+    assert(frame_data != NULL);
+    pcr_helper->last_dts_output = frame->i_dts;
+    pcr_helper->current_media_time -= frame_data->length;
+
+    const vlc_tick_t pcr = transcode_track_pcr_helper_GetFramePCR(pcr_helper, frame_data->dts);
+
+    vlc_list_remove(&frame_data->node);
+    free(frame_data);
+
+    if (pcr == VLC_TICK_INVALID)
+    {
+        return VLC_TICK_INVALID;
+    }
+    return __MIN(frame->i_dts, pcr);
+}


=====================================
modules/stream_out/transcode/pcr_helper.h
=====================================
@@ -0,0 +1,90 @@
+/*****************************************************************************
+ * pcr_helper.h:
+ *****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef PCR_HELPER_H
+#define PCR_HELPER_H
+
+#include <vlc_common.h>
+
+#include <vlc_frame.h>
+
+#include "pcr_sync.h"
+
+/**
+ * The transcode PCR helper is a wrapper of the pcr_sync helper specific for processing
+ * units that have a high risk of altering the frames timestamps or simply dropping them. The usual
+ * case is an encoder and/or a filter chain which is excactly what transcode does.
+ *
+ * This helper uses an approximation of the max frame `delay` taken by the processing unit. When
+ * this delay is bypassed, it is assumed that a frame was dropped and eventually a new PCR can be
+ * synthetised and forwarded to keep the stream going.
+ */
+
+/**
+ * Opaque internal state.
+ */
+typedef struct transcode_track_pcr_helper transcode_track_pcr_helper_t;
+
+/**
+ * Allocate a new pcr helper.
+ *
+ * \param sync_ref Pointer to the pcr_sync, must be valid during all the helper's lifetime.
+ * \param max_delay Maximum frame delay that can be taken by the processing unit (See explanations
+ * above).
+ *
+ * \return A pointer to the allocated pcr helper or NULL if the allocation failed.
+ */
+transcode_track_pcr_helper_t *transcode_track_pcr_helper_New(vlc_pcr_sync_t *sync_ref,
+                                                             vlc_tick_t max_delay);
+
+/**
+ * Delete and free the helper.
+ */
+void transcode_track_pcr_helper_Delete(transcode_track_pcr_helper_t *);
+
+/**
+ * Signal a frame input to the processing unit.
+ *
+ * \param frame The entering frame (will be treated as a single frame).
+ * \param[out] dropped_frame_pcr If a frame was dropped, contains a synthetised PCR.
+ * VLC_TICK_INVALID otherwise.
+ *
+ * \retval VLC_SUCCESS On success.
+ * \retval VLC_ENOMEM On allocation error.
+ */
+int transcode_track_pcr_helper_SignalEnteringFrame(transcode_track_pcr_helper_t *,
+                                                   const vlc_frame_t *frame,
+                                                   vlc_tick_t *dropped_frame_pcr);
+
+/**
+ * Signal a frame output from the processing unit.
+ *
+ * This function return the following PCR to forward if any is available.
+ * \note This can be called multiple times to handle multiple following PCR's.
+ * \note If the frame output has seen its timestamp altered, this helper will synthetise a new PCR.
+ *
+ * \param frame The leaving frame (will be treated as a single frame).
+ *
+ * \return The PCR value following the frame or VLC_TICK_INVALID if no PCR is following.
+ */
+vlc_tick_t transcode_track_pcr_helper_SignalLeavingFrame(transcode_track_pcr_helper_t *,
+                                                         const vlc_frame_t *frame);
+
+#endif


=====================================
modules/stream_out/transcode/pcr_sync.c
=====================================
@@ -0,0 +1,319 @@
+/*****************************************************************************
+ * pcr_sync.c
+ *****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <assert.h>
+#include <stdint.h>
+
+#include <vlc_common.h>
+
+#include <vlc_frame.h>
+#include <vlc_list.h>
+#include <vlc_tick.h>
+#include <vlc_vector.h>
+
+#include "pcr_sync.h"
+
+struct es_dts_entry
+{
+    vlc_tick_t dts;
+    bool passed;
+    vlc_tick_t discontinuity;
+};
+
+typedef struct
+{
+    vlc_tick_t pcr;
+    struct VLC_VECTOR(struct es_dts_entry) es_last_dts_entries;
+    size_t entries_left;
+    bool no_frame_before;
+
+    struct vlc_list node;
+} pcr_event_t;
+
+static inline void pcr_event_Delete(pcr_event_t *ev)
+{
+    vlc_vector_destroy(&ev->es_last_dts_entries);
+    free(ev);
+}
+
+struct es_data
+{
+    bool is_deleted;
+    vlc_tick_t last_frame_dts;
+    vlc_tick_t discontinuity;
+};
+
+static inline struct es_data es_data_Init()
+{
+    return (struct es_data){
+        .is_deleted = false, .last_frame_dts = VLC_TICK_INVALID, .discontinuity = VLC_TICK_INVALID};
+}
+
+struct es_data_vec VLC_VECTOR(struct es_data);
+
+struct vlc_pcr_sync
+{
+    struct vlc_list pcr_events;
+    struct es_data_vec es_data;
+    vlc_mutex_t lock;
+};
+
+vlc_pcr_sync_t *vlc_pcr_sync_New(void)
+{
+    vlc_pcr_sync_t *ret = malloc(sizeof(*ret));
+    if (unlikely(ret == NULL))
+        return NULL;
+
+    vlc_vector_init(&ret->es_data);
+    vlc_list_init(&ret->pcr_events);
+    vlc_mutex_init(&ret->lock);
+    return ret;
+}
+
+void vlc_pcr_sync_Delete(vlc_pcr_sync_t *pcr_sync)
+{
+    vlc_vector_destroy(&pcr_sync->es_data);
+
+    pcr_event_t *it;
+    vlc_list_foreach(it, &pcr_sync->pcr_events, node) { pcr_event_Delete(it); }
+
+    free(pcr_sync);
+}
+
+static bool pcr_sync_ShouldFastForwardPCR(vlc_pcr_sync_t *pcr_sync)
+{
+    struct es_data it;
+    vlc_vector_foreach(it, &pcr_sync->es_data)
+    {
+        if (!it.is_deleted && it.last_frame_dts != VLC_TICK_INVALID)
+            return false;
+    }
+    return vlc_list_is_empty(&pcr_sync->pcr_events);
+}
+
+static bool pcr_sync_HadFrameInputSinceLastPCR(vlc_pcr_sync_t *pcr_sync)
+{
+    const pcr_event_t *pcr_event =
+        vlc_list_last_entry_or_null(&pcr_sync->pcr_events, pcr_event_t, node);
+
+    if (pcr_event == NULL)
+        return true;
+
+    for (unsigned int i = 0; i < pcr_sync->es_data.size; ++i)
+    {
+        const struct es_data *curr = &pcr_sync->es_data.data[i];
+        if (curr->is_deleted || curr->last_frame_dts == VLC_TICK_INVALID)
+            continue;
+
+        const bool is_oob = i >= pcr_event->es_last_dts_entries.size;
+        if (!is_oob && curr->last_frame_dts != pcr_event->es_last_dts_entries.data[i].dts)
+            return true;
+        else if (is_oob)
+            return true;
+    }
+    return false;
+}
+
+static int pcr_sync_SignalPCRLocked(vlc_pcr_sync_t *pcr_sync, vlc_tick_t pcr)
+{
+    if (pcr_sync_ShouldFastForwardPCR(pcr_sync))
+        return VLC_PCR_SYNC_FORWARD_PCR;
+
+    pcr_event_t *event = malloc(sizeof(*event));
+    if (unlikely(event == NULL))
+        return VLC_ENOMEM;
+
+    vlc_vector_init(&event->es_last_dts_entries);
+    const bool alloc_succeeded =
+        vlc_vector_reserve(&event->es_last_dts_entries, pcr_sync->es_data.size);
+    if (unlikely(alloc_succeeded == false))
+    {
+        free(event);
+        return VLC_ENOMEM;
+    }
+
+    size_t entries_left = 0;
+    struct es_data data;
+    vlc_vector_foreach(data, &pcr_sync->es_data)
+    {
+        if (!data.is_deleted)
+        {
+            vlc_vector_push(&event->es_last_dts_entries,
+                            ((struct es_dts_entry){.dts = data.last_frame_dts,
+                                                   .discontinuity = data.discontinuity}));
+            if (data.last_frame_dts != VLC_TICK_INVALID)
+                ++entries_left;
+            data.discontinuity = VLC_TICK_INVALID;
+        }
+    }
+
+    event->pcr = pcr;
+    event->entries_left = entries_left;
+    event->no_frame_before = !pcr_sync_HadFrameInputSinceLastPCR(pcr_sync);
+
+    vlc_list_append(&event->node, &pcr_sync->pcr_events);
+
+    return VLC_SUCCESS;
+}
+
+int vlc_pcr_sync_SignalPCR(vlc_pcr_sync_t *pcr_sync, vlc_tick_t pcr)
+{
+    vlc_mutex_lock(&pcr_sync->lock);
+    const int result = pcr_sync_SignalPCRLocked(pcr_sync, pcr);
+    vlc_mutex_unlock(&pcr_sync->lock);
+    return result;
+}
+
+void vlc_pcr_sync_SignalFrame(vlc_pcr_sync_t *pcr_sync, unsigned int id, const vlc_frame_t *frame)
+{
+    vlc_mutex_lock(&pcr_sync->lock);
+    assert(id < pcr_sync->es_data.size);
+
+    struct es_data *data = &pcr_sync->es_data.data[id];
+    assert(!data->is_deleted);
+    if (frame->i_dts != VLC_TICK_INVALID)
+        data->last_frame_dts = frame->i_dts;
+    if (frame->i_flags & VLC_FRAME_FLAG_DISCONTINUITY)
+    {
+        assert(frame->i_dts != VLC_TICK_INVALID);
+        data->discontinuity = frame->i_dts;
+    }
+
+    vlc_mutex_unlock(&pcr_sync->lock);
+}
+
+int vlc_pcr_sync_NewESID(vlc_pcr_sync_t *pcr_sync, unsigned int *id)
+{
+    vlc_mutex_lock(&pcr_sync->lock);
+
+    for (unsigned int i = 0; i < pcr_sync->es_data.size; ++i)
+    {
+        if (pcr_sync->es_data.data[i].is_deleted)
+        {
+            pcr_sync->es_data.data[i] = es_data_Init();
+            vlc_mutex_unlock(&pcr_sync->lock);
+            *id = i;
+            return VLC_SUCCESS;
+        }
+    }
+
+    const bool push_succeeded = vlc_vector_push(&pcr_sync->es_data, es_data_Init());
+    const size_t ids_count = pcr_sync->es_data.size;
+
+    vlc_mutex_unlock(&pcr_sync->lock);
+
+    if (push_succeeded)
+    {
+        *id = ids_count - 1;
+        return VLC_SUCCESS;
+    }
+    return VLC_ENOMEM;
+}
+
+void vlc_pcr_sync_DelESID(vlc_pcr_sync_t *pcr_sync, unsigned int id)
+{
+    vlc_mutex_lock(&pcr_sync->lock);
+
+    assert(id < pcr_sync->es_data.size);
+    pcr_sync->es_data.data[id].is_deleted = true;
+
+    vlc_mutex_unlock(&pcr_sync->lock);
+}
+
+static inline void pcr_sync_ResetLastReceivedFrames(vlc_pcr_sync_t *pcr_sync)
+{
+    for (size_t i = 0; i < pcr_sync->es_data.size; ++i)
+    {
+        pcr_sync->es_data.data[i].last_frame_dts = VLC_TICK_INVALID;
+    }
+}
+
+vlc_tick_t
+vlc_pcr_sync_SignalFrameOutput(vlc_pcr_sync_t *pcr_sync, unsigned int id, const vlc_frame_t *frame)
+{
+    vlc_mutex_lock(&pcr_sync->lock);
+
+    pcr_event_t *pcr_event = vlc_list_first_entry_or_null(&pcr_sync->pcr_events, pcr_event_t, node);
+
+    if (pcr_event == NULL)
+        goto no_pcr;
+
+    assert(id < pcr_event->es_last_dts_entries.size);
+
+    const vlc_tick_t pcr = pcr_event->pcr;
+
+    if (pcr_event->no_frame_before)
+        goto return_pcr;
+
+    assert(pcr_event->entries_left != 0);
+    struct es_dts_entry *dts_entry = &pcr_event->es_last_dts_entries.data[id];
+    const vlc_tick_t last_dts = dts_entry->dts;
+
+    // Handle scenarios where the current track is ahead of the others in a big way.
+    // When it happen, the PCR threshold of the current track has already been reached and we need
+    // to keep checking the DTS with the next pcr_events entries.
+    if (last_dts == VLC_TICK_INVALID)
+    {
+        pcr_event_t *it;
+        vlc_list_foreach(it, &pcr_sync->pcr_events, node)
+        {
+            if (it == pcr_event)
+                continue;
+
+            struct es_dts_entry *entry = &it->es_last_dts_entries.data[id];
+            if (entry->dts == VLC_TICK_INVALID || entry->passed)
+                continue;
+            if (entry->discontinuity != VLC_TICK_INVALID && frame->i_dts > entry->discontinuity)
+                break;
+            if (frame->i_dts < entry->dts)
+                break;
+
+            entry->passed = true;
+            --it->entries_left;
+            assert(it->entries_left != 0);
+        }
+        goto no_pcr;
+    }
+    if (dts_entry->discontinuity != VLC_TICK_INVALID && frame->i_dts > dts_entry->discontinuity)
+        goto no_pcr;
+
+    if (frame->i_dts < last_dts)
+        goto no_pcr;
+
+    dts_entry->passed = true;
+    if (--pcr_event->entries_left != 0)
+        goto no_pcr;
+
+return_pcr:
+    vlc_list_remove(&pcr_event->node);
+    pcr_event_Delete(pcr_event);
+
+    pcr_sync_ResetLastReceivedFrames(pcr_sync);
+    vlc_mutex_unlock(&pcr_sync->lock);
+    return pcr;
+
+no_pcr:
+    vlc_mutex_unlock(&pcr_sync->lock);
+    return VLC_TICK_INVALID;
+}


=====================================
modules/stream_out/transcode/pcr_sync.h
=====================================
@@ -0,0 +1,121 @@
+/*****************************************************************************
+ * pcr_sync.h
+ *****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef PCR_SYNC_H
+#define PCR_SYNC_H
+
+#if defined(__cplusplus)
+ extern "C" {
+#endif
+
+/**
+ * The PCR-Sync utility is to be used in "stream output filters" such as Transcode where frames are
+ * treated asynchronously and are supposedly sent along with PCR events.
+ *
+ * schematic representation of the problematic:
+ *
+ *                +------------------+
+ * ES-1 Frames -->|   Async Frames   |------>
+ * ES-2 Frames -->|  Processing Unit |------>
+ *                +------------------+
+ *         PCR ----------------------------->
+ *         ^
+ *         +- PCR doesn't go through the processing unit, thus needs to be synchronized to the
+ *            unit's frame output to be relevant.
+ */
+
+/**
+ * Opaque PCR-Sync internal state.
+ */
+typedef struct vlc_pcr_sync vlc_pcr_sync_t;
+
+/**
+ * Allocate and initialize a new pcr_sync.
+ *
+ * \return An heap allocated pcr_sync or NULL if the allocation failed.
+ */
+vlc_pcr_sync_t *vlc_pcr_sync_New(void) VLC_USED;
+
+/**
+ * Delete a pcr_sync and discard its internal state.
+ */
+void vlc_pcr_sync_Delete(vlc_pcr_sync_t *);
+
+/**
+ * Create a new ES ID.
+ *
+ * Create a new id to provide an ES context to frame input.
+ *
+ * \param[out] id The assigned ID if the function is successful, undefined otherwise.
+ *
+ * \retval VLC_SUCCESS On success.
+ * \retval VLC_ENOMEM On internal allocation error.
+ */
+int vlc_pcr_sync_NewESID(vlc_pcr_sync_t *, unsigned int *id);
+
+/**
+ * Delete the given ES ID
+ */
+void vlc_pcr_sync_DelESID(vlc_pcr_sync_t *, unsigned int id);
+
+/**
+ * Signal a frame input from the Frame Processing Unit.
+ *
+ * The frame timestamp will be cached and used in later \ref SignalPCR calls to determine the
+ * correct PCR forwarding point.
+ *
+ * \param id The ES ID (See \ref vlc_pcr_sync_NewESID).
+ */
+void vlc_pcr_sync_SignalFrame(vlc_pcr_sync_t *, unsigned int id, const vlc_frame_t *);
+
+/**
+ * Signal a frame output from the Frame Processing Unit.
+ *
+ * This function return the following PCR to forward if any is available.
+ * \note This can be called multiple times to handle multiple following PCR's.
+ *
+ * \param id The ES ID (See \ref vlc_pcr_sync_NewESID).
+ *
+ * \return The PCR value following the frame or VLC_TICK_INVALID if no PCR is following.
+ */
+vlc_tick_t vlc_pcr_sync_SignalFrameOutput(vlc_pcr_sync_t *, unsigned int id, const vlc_frame_t *);
+
+/**
+ * Status code returned by \ref vlc_pcr_sync_SignalPCR when PCR could be directly forwarded
+ */
+#define VLC_PCR_SYNC_FORWARD_PCR (-EAGAIN)
+
+/**
+ * Signal a PCR event.
+ *
+ * \param pcr The PCR value.
+ *
+ * \retval VLC_SUCCESS On success.
+ * \retval VLC_ENOMEM On allocation error.
+ * \retval VLC_PCR_SYNC_FORWARD_PCR When the Signaled PCR could be immediately sent and is not
+ * queued.
+ */
+int vlc_pcr_sync_SignalPCR(vlc_pcr_sync_t *, vlc_tick_t pcr);
+
+#if defined( __cplusplus )
+}
+#endif
+
+#endif


=====================================
modules/stream_out/transcode/transcode.c
=====================================
@@ -230,6 +230,7 @@ static const char *const ppsz_sout_options[] = {
 static void *Add( sout_stream_t *, const es_format_t * );
 static void  Del( sout_stream_t *, void * );
 static int   Send( sout_stream_t *, void *, block_t * );
+static void  SetPCR(sout_stream_t *, vlc_tick_t );
 
 static void SetAudioEncoderConfig( sout_stream_t *p_stream, transcode_encoder_config_t *p_cfg )
 {
@@ -368,7 +369,7 @@ static int Control( sout_stream_t *p_stream, int i_query, va_list args )
 }
 
 static const struct sout_stream_operations ops = {
-    Add, Del, Send, Control, NULL, NULL,
+    Add, Del, Send, Control, NULL, SetPCR,
 };
 
 /*****************************************************************************
@@ -385,6 +386,13 @@ static int Open( vlc_object_t *p_this )
     config_ChainParse( p_stream, SOUT_CFG_PREFIX, ppsz_sout_options,
                    p_stream->p_cfg );
 
+    p_sys->pcr_sync = vlc_pcr_sync_New();
+    if( unlikely( p_sys->pcr_sync == NULL ) )
+    {
+        free( p_sys );
+        return VLC_ENOMEM;
+    }
+
     /* Audio transcoding parameters */
     transcode_encoder_config_init( &p_sys->aenc_cfg );
     SetAudioEncoderConfig( p_stream, &p_sys->aenc_cfg );
@@ -477,6 +485,8 @@ static void Close( vlc_object_t * p_this )
 
     transcode_encoder_config_clean( &p_sys->senc_cfg );
 
+    vlc_pcr_sync_Delete( p_sys->pcr_sync );
+
     free( p_sys );
 }
 
@@ -648,6 +658,18 @@ static void *Add( sout_stream_t *p_stream, const es_format_t *p_fmt )
     if(!success)
         goto error;
 
+    if( id->b_transcode )
+    {
+        // TODO properly estimate the delay
+        id->pcr_helper = transcode_track_pcr_helper_New( p_sys->pcr_sync, VLC_TICK_FROM_SEC( 2 ) );
+        if( unlikely( id->pcr_helper == NULL ) )
+            goto error;
+    }
+    else
+    {
+        id->pcr_helper = NULL;
+    }
+
     return id;
 
 error:
@@ -694,6 +716,7 @@ static void Del( sout_stream_t *p_stream, void *_id )
         default:
             break;
         }
+        transcode_track_pcr_helper_Delete( id->pcr_helper );
     }
     else decoder_Destroy( id->p_decoder );
 
@@ -718,6 +741,19 @@ static int Send( sout_stream_t *p_stream, void *_id, block_t *p_buffer )
             goto error;
     }
 
+    sout_stream_sys_t *sys = p_stream->p_sys;
+    if( p_buffer != NULL )
+    {
+        assert( p_buffer->p_next == NULL );
+        vlc_tick_t dropped_frame_ts;
+        transcode_track_pcr_helper_SignalEnteringFrame( id->pcr_helper, p_buffer,
+                                                       &dropped_frame_ts );
+        if (dropped_frame_ts != VLC_TICK_INVALID)
+        {
+            sout_StreamSetPCR( p_stream->p_next, dropped_frame_ts );
+        }
+    }
+
     int i_ret;
     switch( id->p_decoder->fmt_in.i_cat )
     {
@@ -737,9 +773,30 @@ static int Send( sout_stream_t *p_stream, void *_id, block_t *p_buffer )
         goto error;
     }
 
-    if( p_out &&
-        sout_StreamIdSend( p_stream->p_next, id->downstream_id, p_out ) )
-        i_ret = VLC_EGENERIC;
+    if( p_out )
+    {
+        for( block_t *it = p_out; it != NULL; )
+        {
+            block_t *next = it->p_next;
+            it->p_next = NULL;
+
+            const vlc_tick_t pcr =
+                transcode_track_pcr_helper_SignalLeavingFrame( id->pcr_helper, it );
+
+            if( sout_StreamIdSend( p_stream->p_next, id->downstream_id, it ) != VLC_SUCCESS )
+            {
+                p_buffer = next;
+                goto error;
+            }
+
+            if( pcr != VLC_TICK_INVALID )
+            {
+                sout_StreamSetPCR( p_stream->p_next, pcr );
+            }
+
+            it = next;
+        }
+    }
 
     if (i_ret != VLC_SUCCESS)
         id->b_error = true;
@@ -750,3 +807,13 @@ error:
         block_Release( p_buffer );
     return VLC_EGENERIC;
 }
+
+static void SetPCR( sout_stream_t *stream, vlc_tick_t pcr )
+{
+    sout_stream_sys_t *sys = stream->p_sys;
+    const int status = vlc_pcr_sync_SignalPCR( sys->pcr_sync, pcr );
+    if ( status == VLC_PCR_SYNC_FORWARD_PCR )
+    {
+        sout_StreamSetPCR( stream->p_next, pcr );
+    }
+}


=====================================
modules/stream_out/transcode/transcode.h
=====================================
@@ -2,6 +2,7 @@
 #include <vlc_filter.h>
 #include <vlc_codec.h>
 #include "encoder/encoder.h"
+#include "pcr_helper.h"
 
 /*100ms is around the limit where people are noticing lipsync issues*/
 #define MASTER_SYNC_MAX_DRIFT VLC_TICK_FROM_MS(100)
@@ -64,6 +65,7 @@ typedef struct
     /* Spu's video */
     sout_stream_id_sys_t *id_video;
 
+    vlc_pcr_sync_t *pcr_sync;
 } sout_stream_sys_t;
 
 struct aout_filters;
@@ -147,6 +149,8 @@ struct sout_stream_id_sys_t
     /* Sync */
     date_t          next_input_pts; /**< Incoming calculated PTS */
     vlc_tick_t      i_drift; /** how much buffer is ahead of calculated PTS */
+
+    transcode_track_pcr_helper_t *pcr_helper;
 };
 
 struct decoder_owner


=====================================
test/Makefile.am
=====================================
@@ -47,6 +47,7 @@ check_PROGRAMS = \
 	test_modules_demux_ts_pes \
 	test_modules_playlist_m3u \
 	test_modules_stream_out_transcode \
+	test_modules_stream_out_pcr_sync \
 	$(NULL)
 
 if ENABLE_SOUT
@@ -184,6 +185,11 @@ test_modules_stream_out_transcode_SOURCES = \
 	modules/stream_out/transcode_scenarios.c
 test_modules_stream_out_transcode_LDADD = $(LIBVLCCORE) $(LIBVLC)
 
+test_modules_stream_out_pcr_sync_SOURCES = modules/stream_out/pcr_sync.c \
+	../modules/stream_out/transcode/pcr_sync.c \
+	../modules/stream_out/transcode/pcr_sync.h
+test_modules_stream_out_pcr_sync_LDADD = $(LIBVLCCORE)
+
 checkall:
 	$(MAKE) check_PROGRAMS="$(check_PROGRAMS) $(EXTRA_PROGRAMS)" check
 


=====================================
test/modules/stream_out/pcr_sync.c
=====================================
@@ -0,0 +1,235 @@
+/*****************************************************************************
+ * src/test/pcr_delayer.c
+ *****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#undef NDEBUG
+
+#include <assert.h>
+
+#include <vlc_common.h>
+
+#include <vlc_arrays.h>
+#include <vlc_frame.h>
+#include <vlc_tick.h>
+#include <vlc_vector.h>
+
+#include "../modules/stream_out/transcode/pcr_sync.h"
+
+typedef struct
+{
+    enum test_element_type
+    {
+        TEST_ELEM_TYPE_FRAME,
+        TEST_ELEM_TYPE_PCR,
+    } type;
+    union
+    {
+        struct
+        {
+            vlc_tick_t dts;
+            unsigned int track_number;
+            bool discontinuity;
+        };
+        vlc_tick_t pcr;
+    };
+} test_element;
+
+typedef struct
+{
+    size_t input_count;
+    const test_element *input;
+    size_t output_count;
+    const test_element *output;
+
+    size_t track_count;
+} test_context;
+
+#undef FRAME
+#define FRAME(id, val)                                                                             \
+    ((test_element){.type = TEST_ELEM_TYPE_FRAME, .dts = val, .track_number = id})
+#undef DISCONTINUITY
+#define DISCONTINUITY(id, val)                                                                     \
+    ((test_element){                                                                               \
+        .type = TEST_ELEM_TYPE_FRAME, .dts = val, .track_number = id, .discontinuity = true})
+#undef PCR
+#define PCR(val) ((test_element){.type = TEST_ELEM_TYPE_PCR, .pcr = val})
+
+static void test_Sync(vlc_pcr_sync_t *sync, const test_context context)
+{
+    // Add all tracks to the synchronizer and keep their id.
+    struct VLC_VECTOR(unsigned int) track_ids_vec = VLC_VECTOR_INITIALIZER;
+    bool alloc_success = vlc_vector_reserve(&track_ids_vec, context.track_count);
+    assert(alloc_success);
+    for (size_t i = 0; i < context.track_count; ++i)
+    {
+        unsigned int id;
+        alloc_success = vlc_pcr_sync_NewESID(sync, &id) == VLC_SUCCESS;
+        assert(alloc_success);
+        alloc_success = vlc_vector_push(&track_ids_vec, id);
+        assert(alloc_success);
+    }
+
+    // Queue PCR events in the synchronizer and signal all the frames.
+    for (size_t i = 0; i < context.input_count; ++i)
+    {
+        const test_element *elem = &context.input[i];
+        if (elem->type == TEST_ELEM_TYPE_PCR)
+            vlc_pcr_sync_SignalPCR(sync, elem->pcr);
+        else
+        {
+            const vlc_frame_t mock = {.i_dts = elem->dts,
+                                      .i_flags =
+                                          elem->discontinuity ? VLC_FRAME_FLAG_DISCONTINUITY : 0};
+            vlc_pcr_sync_SignalFrame(sync, track_ids_vec.data[elem->track_number], &mock);
+        }
+    }
+
+    // Signal all the frame leaving and check that PCR events are properly aligned.
+    vlc_tick_t last_pcr;
+    for (size_t i = 0; i < context.output_count; ++i)
+    {
+        const test_element *elem = &context.output[i];
+        if (elem->type == TEST_ELEM_TYPE_FRAME)
+        {
+            const vlc_frame_t mock = {.i_dts = elem->dts};
+            last_pcr =
+                vlc_pcr_sync_SignalFrameOutput(sync, track_ids_vec.data[elem->track_number], &mock);
+        }
+        else
+        {
+            assert(elem->pcr == last_pcr);
+        }
+    }
+
+    vlc_vector_destroy(&track_ids_vec);
+}
+
+static void test_Run(void (*test)(vlc_pcr_sync_t *))
+{
+    vlc_pcr_sync_t *sync = vlc_pcr_sync_New();
+    assert(sync != NULL);
+
+    test(sync);
+
+    vlc_pcr_sync_Delete(sync);
+}
+
+typedef enum
+{
+    VIDEO_TRACK,
+    AUDIO_TRACK,
+    SUB_TRACK,
+
+    MAX_TRACKS
+} track_ids;
+
+static void test_Simple(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {FRAME(VIDEO_TRACK, 1), PCR(10), FRAME(VIDEO_TRACK, 20)};
+    test_Sync(sync, (test_context){
+                        .input = input,
+                        .input_count = ARRAY_SIZE(input),
+                        .output = input,
+                        .output_count = ARRAY_SIZE(input),
+                        .track_count = 1,
+                    });
+}
+
+static void test_MultipleTracks(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {
+        FRAME(VIDEO_TRACK, 1),  FRAME(AUDIO_TRACK, 1),  PCR(10),
+        FRAME(VIDEO_TRACK, 15), FRAME(AUDIO_TRACK, 15), FRAME(SUB_TRACK, 20)};
+    test_Sync(sync, (test_context){.input = input,
+                                   .input_count = ARRAY_SIZE(input),
+                                   .output = input,
+                                   .output_count = ARRAY_SIZE(input),
+                                   .track_count = MAX_TRACKS});
+}
+
+static void test_MultipleTracksButOnlyOneSends(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {FRAME(VIDEO_TRACK, 1), PCR(10), FRAME(VIDEO_TRACK, 15)};
+    test_Sync(sync, (test_context){.input = input,
+                                   .input_count = ARRAY_SIZE(input),
+                                   .output = input,
+                                   .output_count = ARRAY_SIZE(input),
+                                   .track_count = MAX_TRACKS});
+}
+
+static void test_InvalidDTS(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {FRAME(VIDEO_TRACK, 1), FRAME(VIDEO_TRACK, VLC_TICK_INVALID),
+                                  FRAME(VIDEO_TRACK, VLC_TICK_INVALID), PCR(5),
+                                  FRAME(VIDEO_TRACK, 10)};
+    const test_element output[] = {FRAME(VIDEO_TRACK, 1), PCR(5), FRAME(VIDEO_TRACK, 10)};
+    test_Sync(sync, (test_context){.input = input,
+                                   .input_count = ARRAY_SIZE(input),
+                                   .output = output,
+                                   .output_count = ARRAY_SIZE(output),
+                                   .track_count = MAX_TRACKS});
+}
+
+static void test_LowDiscontinuity(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {FRAME(VIDEO_TRACK, 200), DISCONTINUITY(VIDEO_TRACK, 1), PCR(5),
+                                  FRAME(VIDEO_TRACK, 10)};
+    test_Sync(sync, (test_context){.input = input,
+                                   .input_count = ARRAY_SIZE(input),
+                                   .output = input,
+                                   .output_count = ARRAY_SIZE(input),
+                                   .track_count = MAX_TRACKS});
+}
+
+static void test_HighDiscontinuity(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {FRAME(VIDEO_TRACK, 1), DISCONTINUITY(VIDEO_TRACK, 200), PCR(210),
+                                  FRAME(VIDEO_TRACK, 220)};
+    test_Sync(sync, (test_context){.input = input,
+                                   .input_count = ARRAY_SIZE(input),
+                                   .output = input,
+                                   .output_count = ARRAY_SIZE(input),
+                                   .track_count = MAX_TRACKS});
+}
+static void test_TwoDiscontinuities(vlc_pcr_sync_t *sync)
+{
+    const test_element input[] = {FRAME(VIDEO_TRACK, 200), DISCONTINUITY(VIDEO_TRACK, 1),
+                                  DISCONTINUITY(VIDEO_TRACK, 210), PCR(210),
+                                  FRAME(VIDEO_TRACK, 220)};
+    test_Sync(sync, (test_context){.input = input,
+                                   .input_count = ARRAY_SIZE(input),
+                                   .output = input,
+                                   .output_count = ARRAY_SIZE(input),
+                                   .track_count = MAX_TRACKS});
+}
+
+int main()
+{
+    test_Run(test_Simple);
+    test_Run(test_MultipleTracks);
+    test_Run(test_MultipleTracksButOnlyOneSends);
+    test_Run(test_InvalidDTS);
+    test_Run(test_LowDiscontinuity);
+    test_Run(test_HighDiscontinuity);
+    test_Run(test_TwoDiscontinuities);
+}



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/fe03c25a6b05dac77d583ce8ab2f572b598677a3...57ce124f144606849135f1702505f431c0d86422

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/fe03c25a6b05dac77d583ce8ab2f572b598677a3...57ce124f144606849135f1702505f431c0d86422
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list