/* demuxmpeg.c

   Copyright (c) 2003-2026 HandBrake Team
   This file is part of the HandBrake source code
   Homepage: <http://handbrake.fr/>.
   It may be used under the terms of the GNU General Public License v2.
   For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html
 */

#include "libavutil/avutil.h"
#include "handbrake/handbrake.h"

static inline int check_mpeg_scr( hb_psdemux_t *state, int64_t scr, int tol )
{
    /*
     * This section of code implements the timing model of
     * the "Standard Target Decoder" (STD) of the MPEG2 standard
     * (specified in ISO 13818-1 sections 2.4.2, 2.5.2 & Annex D).
     * The STD removes and corrects for clock discontinuities so
     * that the time stamps on the video, audio & other media
     * streams can be used for cross-media synchronization. To do
     * this the STD has its own timestamp value, the System Clock
     * Reference or SCR, in the PACK header. Clock discontinuities
     * are detected using the SCR & and the adjustment needed
     * to correct post-discontinuity timestamps to be contiguous
     * with pre-discontinuity timestamps is computed from pre- and
     * post-discontinuity values of the SCR. Then this adjustment
     * is applied to every media timestamp (PTS).
     *
     * ISO 13818-1 says there must be an SCR at least every 700ms
     * (100ms for Transport Streams) so if the difference between
     * this SCR & the previous is >700ms it's a discontinuity.
     * If the difference is negative it's non-physical (time doesn't
     * go backward) and must also be a discontinuity. When we find a
     * discontinuity we adjust the scr_offset so that the SCR of the
     * new packet lines up with that of the previous packet.
     */

    // we declare a discontinuity if there's a gap of more than
    // 'tol'ms between the last scr & this or if this scr goes back
    // by more than half a frame time.
    int discontinuity = 0;
    int64_t scr_delta = scr - state->last_scr;
    if (state->last_scr == AV_NOPTS_VALUE ||
        scr_delta > 90*tol || scr_delta < -90*10)
    {
        ++state->scr_changes;
        state->last_pts = AV_NOPTS_VALUE;
        discontinuity = 1;
    }
    state->last_scr = scr;
    return discontinuity;
}

static inline void save_chap( hb_psdemux_t *state, hb_buffer_t *buf )
{
    if ( state && buf->s.new_chap )
    {
        state->new_chap = buf->s.new_chap;
        buf->s.new_chap = 0;
    }
}

static inline void restore_chap( hb_psdemux_t *state, hb_buffer_t *buf )
{
    if ( state )
    {
        buf->s.new_chap = state->new_chap;
        state->new_chap = 0;
    }
}

/* Basic MPEG demuxer */

static void demux_dvd_ps( hb_buffer_t * buf, hb_buffer_list_t * list_es,
                          hb_psdemux_t* state )
{
    hb_buffer_t * buf_es;
    int           pos = 0;

    while ( buf )
    {
        save_chap( state, buf );

#define d (buf->data)
        /* pack_header */
        if( d[pos] != 0 || d[pos+1] != 0 ||
            d[pos+2] != 0x1 || d[pos+3] != 0xBA )
        {
            hb_log( "demux_dvd_ps: not a PS packet (%02x%02x%02x%02x)",
                    d[pos], d[pos+1], d[pos+2], d[pos+3] );
            hb_buffer_t *tmp = buf->next;
            buf->next = NULL;
            hb_buffer_close( &buf );
            buf = tmp;
            continue;
        }
        pos += 4;                    /* pack_start_code */

        if ( state )
        {
            /* extract the system clock reference (scr) */
            int64_t scr = ((uint64_t)(d[pos] & 0x38) << 27) |
                          ((uint64_t)(d[pos] & 0x03) << 28) |
                          ((uint64_t)(d[pos+1]) << 20) |
                          ((uint64_t)(d[pos+2] >> 3) << 15) |
                          ((uint64_t)(d[pos+2] & 3) << 13) |
                          ((uint64_t)(d[pos+3]) << 5) |
                          (d[pos+4] >> 3);
            check_mpeg_scr( state, scr, 700 );
        }

        pos += 9;                    /* pack_header */
        pos += 1 + ( d[pos] & 0x7 ); /* stuffing bytes */

        /* system_header */
        if( d[pos] == 0 && d[pos+1] == 0 &&
            d[pos+2] == 0x1 && d[pos+3] == 0xBB )
        {
            int header_length;

            pos           += 4; /* system_header_start_code */
            header_length  = ( d[pos] << 8 ) + d[pos+1];
            pos           += 2 + header_length;
        }

        /* pes */
        while( pos + 6 < buf->size &&
               d[pos] == 0 && d[pos+1] == 0 && d[pos+2] == 0x1 )
        {
            int      id;
            int      pes_packet_length;
            int      pes_packet_end;
            int      pes_header_d_length;
            int      pes_header_end;
            int      has_pts;
            int64_t  pts = AV_NOPTS_VALUE, dts = AV_NOPTS_VALUE;

            pos               += 3;               /* packet_start_code_prefix */
            id           = d[pos];
            pos               += 1;

            /* pack_header */
            if( id == 0xBA)
            {
                pos += 10 + (d[pos+9] & 7);
                continue;
            }

            /* system_header */
            if( id == 0xBB )
            {
                int header_length;

                header_length  = ( d[pos] << 8 ) + d[pos+1];
                pos           += 2 + header_length;
                continue;
            }

            pes_packet_length  = ( d[pos] << 8 ) + d[pos+1];
            pos               += 2;               /* pes_packet_length */
            pes_packet_end     = pos + pes_packet_length;

            if( id != 0xE0 && id != 0xBD &&
                ( id & 0xC0 ) != 0xC0  )
            {
                /* Not interesting */
                pos = pes_packet_end;
                continue;
            }

            has_pts            = d[pos+1] >> 6;
            pos               += 2;               /* Required headers */

            pes_header_d_length  = d[pos];
            pos                    += 1;
            pes_header_end          = pos + pes_header_d_length;

            if( has_pts )
            {
                pts = ( (uint64_t)(d[pos] & 0xe ) << 29 ) +
                      ( d[pos+1] << 22 ) +
                      ( ( d[pos+2] >> 1 ) << 15 ) +
                      ( d[pos+3] << 7 ) +
                      ( d[pos+4] >> 1 );
                if ( has_pts & 1 )
                {
                    dts = ( (uint64_t)(d[pos+5] & 0xe ) << 29 ) +
                          ( d[pos+6] << 22 ) +
                          ( ( d[pos+7] >> 1 ) << 15 ) +
                          ( d[pos+8] << 7 ) +
                          ( d[pos+9] >> 1 );
                }
                else
                {
                    dts = pts;
                }
            }

            pos = pes_header_end;

            if( id == 0xBD )
            {
                id |= ( d[pos] << 8 );
                if( ( id & 0xF0FF ) == 0x80BD ) /* A52 */
                {
                    pos += 4;
                }
                else if( ( id & 0xE0FF ) == 0x20BD || /* SPU */
                         ( id & 0xF0FF ) == 0xA0BD )  /* LPCM */
                {
                    pos += 1;
                }
            }

            /* Sanity check */
            if( pos >= pes_packet_end )
            {
                pos = pes_packet_end;
                continue;
            }

            /* Here we hit we ES payload */
            buf_es = hb_buffer_init( pes_packet_end - pos );

            buf_es->s.id           = id;
            buf_es->s.start        = pts;
            buf_es->s.renderOffset = dts;
            buf_es->s.duration     = (int64_t)AV_NOPTS_VALUE;
            buf_es->s.stop         = AV_NOPTS_VALUE;
            if ( state && id == 0xE0)
            {
                // Consume a chapter break, and apply it to the ES.
                restore_chap( state, buf_es );
            }
            memcpy( buf_es->data, d + pos, pes_packet_end - pos );

            hb_buffer_list_append(list_es, buf_es);

            pos = pes_packet_end;
        }
        hb_buffer_t *tmp = buf->next;
        buf->next = NULL;
        hb_buffer_close( &buf );
        buf = tmp;
    }
#undef d
}

// mpeg transport stream demuxer. the elementary stream headers have been
// stripped off and buf has all the info gleaned from them: id is set,
// start contains the pts (if any), renderOffset contains the dts (if any)
// and stop contains the pcr (if it changed).
static void demux_mpeg( hb_buffer_t *buf, hb_buffer_list_t *list_es,
                        hb_psdemux_t *state, int tolerance )
{
    while ( buf )
    {
        save_chap( state, buf );
        if ( state )
        {
            int discontinuity = 0;
            // we're keeping track of timing (i.e., not in scan)
            // check if there's a new pcr in this packet
            if ( buf->s.pcr >= 0 )
            {
                // we have a new pcr
                discontinuity = check_mpeg_scr( state, buf->s.pcr, tolerance );
                buf->s.pcr = AV_NOPTS_VALUE;
                // Some streams have consistently bad PCRs or SCRs
                // So filter out the offset
                if ( buf->s.start >= 0 )
                    state->scr_delta = buf->s.start - state->last_scr;
                else
                    state->scr_delta = 0;
            }
            if ( !discontinuity && buf->s.discontinuity )
            {
                // Buffer has been flagged as a discontinuity.  This happens
                // when a blueray changes clips.
                ++state->scr_changes;
                state->last_scr = buf->s.start;
                state->scr_delta = 0;
            }

            if ( buf->s.start >= 0 )
            {
                int64_t fdelta;
                if (buf->s.type == AUDIO_BUF || buf->s.type == VIDEO_BUF)
                {
                    if ( state->last_pts >= 0 )
                    {
                        fdelta = buf->s.start - state->last_pts;
                        if ( fdelta < -5 * 90000LL || fdelta > 5 * 90000LL )
                        {
                            // Packet too far from last. This may be a NZ TV
                            // broadcast as they like to change the PCR without
                            // sending a PCR update. Since it may be a while
                            // until they actually tell us the new PCR use the
                            // PTS as the PCR.
                            ++state->scr_changes;
                            state->last_scr = buf->s.start;
                            state->scr_delta = 0;
                        }
                    }
                    state->last_pts = buf->s.start;
                }
                if (state->last_scr != AV_NOPTS_VALUE)
                {
                    // Program streams have an SCR in every PACK header so they
                    // can't lose their clock reference. But the PCR in
                    // Transport streams is typically on <.1% of the packets.
                    // If a PCR packet gets lost and it marks a clock
                    // discontinuity then the data following it will be
                    // referenced to the wrong clock & introduce huge gaps or
                    // throw our A/V sync off. We try to protect against that
                    // here by sanity checking timestamps against the current
                    // reference clock and discarding packets where the DTS
                    // is "too far" from its clock.
                    fdelta = buf->s.start - state->last_scr - state->scr_delta;
                    if ( fdelta < -300 * 90000LL || fdelta > 300 * 90000LL )
                    {
                        // packet too far behind or ahead of its clock reference
                        buf->s.renderOffset = AV_NOPTS_VALUE;
                        buf->s.start = AV_NOPTS_VALUE;
                        buf->s.stop = AV_NOPTS_VALUE;
                    }
                    else
                    {
                        // Some streams have no PCRs.  In these cases, we
                        // will only get an "PCR" update if a large change
                        // in DTS or PTS is detected.  So we need to update
                        // our scr_delta with each valid timestamp so that
                        // fdelta does not continually grow.
                        state->scr_delta = buf->s.start - state->last_scr;
                    }
                }
            }

            if ( buf->s.type == VIDEO_BUF )
            {
                restore_chap( state, buf );
            }
        }

        hb_buffer_t *tmp = buf->next;
        buf->next = NULL;
        hb_buffer_list_append(list_es, buf);
        buf = tmp;
    }
}

static void demux_ts( hb_buffer_t *buf, hb_buffer_list_t *list_es,
                      hb_psdemux_t *state )
{
    // Distance between PCRs in TS is up to 100ms, but we have seen
    // streams that exceed this, so allow up to 300ms.
    demux_mpeg(buf, list_es, state, 300);
}

static void demux_ps( hb_buffer_t *buf, hb_buffer_list_t *list_es,
                      hb_psdemux_t *state )
{
    // Distance between SCRs in PS is up to 700ms
    demux_mpeg(buf, list_es, state, 700);
}

// "null" demuxer (makes a copy of input buf & returns it in list)
// used when the reader for some format includes its own demuxer.
// for example, ffmpeg.
static void demux_null( hb_buffer_t * buf, hb_buffer_list_t * list_es,
                        hb_psdemux_t* state )
{
    while ( buf )
    {
        save_chap( state, buf );
        if ( state )
        {
            // if we don't have a time offset yet,
            // use this timestamp as the offset.
            if (state->scr_changes == 0 &&
                (buf->s.start != AV_NOPTS_VALUE ||
                 buf->s.renderOffset != AV_NOPTS_VALUE))
            {
                ++state->scr_changes;
                state->last_scr = buf->s.start >= 0 ? buf->s.start : buf->s.renderOffset;
            }

            if ( buf->s.type == VIDEO_BUF )
            {
                restore_chap( state, buf );
            }
        }

        hb_buffer_t *tmp = buf->next;
        buf->next = NULL;
        hb_buffer_list_append(list_es, buf);
        buf = tmp;
    }
}

const hb_muxer_t hb_demux[] = { demux_dvd_ps, demux_ts, demux_ps, demux_null };
