/* bd.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 "handbrake/handbrake.h"
#include "handbrake/lang.h"
#include "handbrake/hbffmpeg.h"

#include "libbluray/bluray.h"

struct hb_bd_s
{
    char                    * path;
    BLURAY                  * bd;
    int                       title_count;
    BLURAY_TITLE_INFO      ** title_info;
    const BLURAY_DISC_INFO  * disc_info;
    int64_t                   duration;
    hb_stream_t             * stream;
    int                       chapter;
    int                       next_chap;
    hb_handle_t             * h;
    int                       keep_duplicate_titles;
};

/***********************************************************************
 * Local prototypes
 **********************************************************************/
static int           next_packet( BLURAY *bd, uint8_t *pkt );
static int title_info_compare_mpls(const void *, const void *);

/***********************************************************************
 * hb_bd_init
 ***********************************************************************
 *
 **********************************************************************/
hb_bd_t * hb_bd_init( hb_handle_t *h, const char * path, int keep_duplicate_titles )
{
    hb_bd_t * d;
    int ii;

    d = calloc( sizeof( hb_bd_t ), 1 );
    d->h = h;
    d->keep_duplicate_titles = keep_duplicate_titles;

    /* Open device */
    d->bd = bd_open( path, NULL );
    if( d->bd == NULL )
    {
        /*
         * Not an error, may be a stream - which we'll try in a moment.
         */
        hb_log( "bd: not a bd - trying as a stream/file instead" );
        goto fail;
    }

    uint8_t flags = TITLES_FILTER_DUP_CLIP;
    if (!keep_duplicate_titles)
    {
        flags |= TITLES_FILTER_DUP_TITLE;
    }
    d->title_count = bd_get_titles( d->bd, flags, 0 );
    if ( d->title_count == 0 )
    {
        hb_log( "bd: not a bd - trying as a stream/file instead" );
        goto fail;
    }
    d->title_info = calloc( sizeof( BLURAY_TITLE_INFO* ) , d->title_count );
    for ( ii = 0; ii < d->title_count; ii++ )
    {
        d->title_info[ii] = bd_get_title_info( d->bd, ii, 0 );
    }
    qsort(d->title_info, d->title_count, sizeof( BLURAY_TITLE_INFO* ), title_info_compare_mpls );
    d->disc_info = bd_get_disc_info(d->bd);
    d->path = strdup( path );

    return d;

fail:
    if( d->bd ) bd_close( d->bd );
    free( d );
    return NULL;
}

/***********************************************************************
 * hb_bd_title_count
 **********************************************************************/
int hb_bd_title_count( hb_bd_t * d )
{
    return d->title_count;
}

static void add_subtitle(int track, hb_list_t *list_subtitle, BLURAY_STREAM_INFO *bdsub, uint32_t codec, uint32_t codec_param)
{
    hb_subtitle_t * subtitle;
    iso639_lang_t * lang;

    subtitle = calloc( sizeof( hb_subtitle_t ), 1 );

    subtitle->track = track;
    subtitle->id = bdsub->pid;

    switch ( bdsub->coding_type )
    {
        case BLURAY_STREAM_TYPE_SUB_PG:
            subtitle->source = PGSSUB;
            subtitle->format = PICTURESUB;
            subtitle->config.dest = RENDERSUB;
            break;
        default:
            // Unrecognized, don't add to list
            free( subtitle );
            return;
    }
    lang = lang_for_code2( (char*)bdsub->lang );
    snprintf(subtitle->lang, sizeof( subtitle->lang ), "%s (%s)",
             strlen(lang->native_name) ? lang->native_name : lang->eng_name,
             hb_subsource_name(subtitle->source));
    snprintf(subtitle->iso639_2, sizeof( subtitle->iso639_2 ), "%s",
             lang->iso639_2);

    subtitle->reg_desc     = STR4_TO_UINT32("HDMV");
    subtitle->stream_type  = bdsub->coding_type;
    subtitle->codec        = codec;
    subtitle->codec_param  = codec_param;
    subtitle->timebase.num = 1;
    subtitle->timebase.den = 90000;

    hb_log( "bd: subtitle id=0x%x, lang=%s, 3cc=%s", subtitle->id,
            subtitle->lang, subtitle->iso639_2 );

    hb_list_add( list_subtitle, subtitle );
    return;
}

static void add_audio(int index, int linked_index, int track, hb_list_t *list_audio, BLURAY_STREAM_INFO *bdaudio, int substream_type, uint32_t codec, uint32_t codec_param, int attributes)
{
    const char * codec_name;
    hb_audio_t * audio;
    iso639_lang_t * lang;

    audio = calloc( sizeof( hb_audio_t ), 1 );

    audio->id = (substream_type << 16) | bdaudio->pid;
    audio->config.index = index;
    if (linked_index >= 0)
    {
        audio->config.list_linked_index = hb_list_init();
        hb_list_add_dup(audio->config.list_linked_index,
                        &linked_index, sizeof(linked_index));
    }
    audio->config.in.reg_desc = STR4_TO_UINT32("HDMV");
    audio->config.in.stream_type = bdaudio->coding_type;
    audio->config.in.substream_type = substream_type;
    audio->config.in.codec = codec;
    audio->config.in.codec_param = codec_param;

    switch( audio->config.in.codec )
    {
        case HB_ACODEC_AC3:
            codec_name = "AC3";
            break;
        case HB_ACODEC_DCA:
            codec_name = "DTS";
            break;
        default:
        {
            if( audio->config.in.codec & HB_ACODEC_FF_MASK )
            {
                switch( bdaudio->coding_type )
                {
                    case BLURAY_STREAM_TYPE_AUDIO_AC3PLUS:
                        codec_name = "E-AC3";
                        break;
                    case BLURAY_STREAM_TYPE_AUDIO_DTSHD:
                        codec_name = "DTS-HD HRA";
                        break;
                    case BLURAY_STREAM_TYPE_AUDIO_DTSHD_MASTER:
                        codec_name = "DTS-HD MA";
                        break;
                    case BLURAY_STREAM_TYPE_AUDIO_LPCM:
                        codec_name = "BD LPCM";
                        break;
                    case BLURAY_STREAM_TYPE_AUDIO_MPEG1:
                        codec_name = "MPEG1";
                        break;
                    case BLURAY_STREAM_TYPE_AUDIO_MPEG2:
                        codec_name = "MPEG2";
                        break;
                    case BLURAY_STREAM_TYPE_AUDIO_TRUHD:
                        codec_name = "TrueHD";
                        break;
                    default:
                        codec_name = "Unknown FFmpeg";
                        break;
                }
            }
            else
            {
                codec_name = "Unknown";
            }
        }
        break;
    }

    lang = lang_for_code2( (char*)bdaudio->lang );

    audio->config.lang.attributes = attributes;

    snprintf( audio->config.lang.simple,
              sizeof( audio->config.lang.simple ), "%s",
              strlen( lang->native_name ) ? lang->native_name : lang->eng_name );
    snprintf( audio->config.lang.iso639_2,
              sizeof( audio->config.lang.iso639_2 ), "%s", lang->iso639_2 );

    hb_log("bd: audio id=0x%x, lang=%s (%s), 3cc=%s", audio->id,
           audio->config.lang.simple, codec_name, audio->config.lang.iso639_2);

    audio->config.in.track        = track;
    audio->config.in.timebase.num = 1;
    audio->config.in.timebase.den = 90000;

    hb_list_add( list_audio, audio );
    return;
}

static int bd_audio_equal( BLURAY_CLIP_INFO *a, BLURAY_CLIP_INFO *b )
{
    int ii, jj, equal;

    if ( a->audio_stream_count != b->audio_stream_count )
        return 0;

    if ( a->audio_stream_count == 0 )
        return 0;

    for ( ii = 0; ii < a->audio_stream_count; ii++ )
    {
        BLURAY_STREAM_INFO * s = &a->audio_streams[ii];
        equal = 0;
        for ( jj = 0; jj < b->audio_stream_count; jj++ )
        {
            if ( s->pid == b->audio_streams[jj].pid &&
                 s->coding_type == b->audio_streams[jj].coding_type)
            {
                equal = 1;
                break;
            }
        }
        if ( !equal )
            return 0;
    }
    return 1;
}

static void show_clip_list( BLURAY_TITLE_INFO * ti )
{
    int ii;

    for (ii = 0; ii < ti->clip_count; ii++)
    {
        BLURAY_CLIP_INFO * ci = &ti->clips[ii];
        int64_t            duration = ci->out_time - ci->in_time;
        int                hh, mm, ss;

        hh = duration / (90000 * 60 * 60);
        mm = (duration / (90000 * 60)) % 60;
        ss = (duration / 90000) % 60;
        hb_log("bd:\t\t%s.M2TS -- Duration: %02d:%02d:%02d",
               ti->clips[ii].clip_id, hh, mm, ss);
    }
}

/***********************************************************************
 * hb_bd_title_scan
 **********************************************************************/
hb_title_t * hb_bd_title_scan( hb_bd_t * d, int tt, uint64_t min_duration, uint64_t max_duration )
{

    hb_title_t   * title;
    hb_chapter_t * chapter;
    int            ii, jj;
    BLURAY_TITLE_INFO * ti = NULL;

    hb_log( "bd: scanning title %d", tt );

    title = hb_title_init( d->path, tt );
    title->demuxer = HB_TS_DEMUXER;
    title->type = HB_BD_TYPE;
    title->reg_desc = STR4_TO_UINT32("HDMV");
    title->keep_duplicate_titles = d->keep_duplicate_titles;

    if (d->disc_info->disc_name != NULL && d->disc_info->disc_name[0] != 0)
    {
        title->name = strdup(d->disc_info->disc_name);
    }
    else if (d->disc_info->udf_volume_id != NULL &&
             d->disc_info->udf_volume_id[0] != 0)
    {
        title->name = strdup(d->disc_info->udf_volume_id);
    }
    else
    {
        char * p_cur, * p_last = d->path;
        for( p_cur = d->path; *p_cur; p_cur++ )
        {
            if( IS_DIR_SEP(p_cur[0]) && p_cur[1] )
            {
                p_last = &p_cur[1];
            }
        }
        title->name = strdup(p_last);
        char *dot_term = strrchr(title->name, '.');
        if (dot_term)
            *dot_term = '\0';
    }

    if (tt <= d->title_count)
    {
        ti = d->title_info[tt - 1];
    }
    if ( ti == NULL )
    {
        hb_log( "bd: invalid title" );
        goto fail;
    }
    if ( ti->clip_count == 0 )
    {
        hb_log( "bd: stream has no clips" );
        goto fail;
    }
    if ( ti->clips[0].video_stream_count == 0 )
    {
        hb_log( "bd: stream has no video" );
        goto fail;
    }

    hb_log( "bd: playlist %05d.MPLS", ti->playlist );
    title->playlist = ti->playlist;
    title->angle_count = ti->angle_count;

    /* Get duration */
    title->duration = ti->duration;
    title->hours    = title->duration / 90000 / 3600;
    title->minutes  = ( ( title->duration / 90000 ) % 3600 ) / 60;
    title->seconds  = ( title->duration / 90000 ) % 60;
    hb_log( "bd: duration is %02d:%02d:%02d (%"PRIu64" ms)",
            title->hours, title->minutes, title->seconds,
            title->duration / 90 );

    /* ignore short titles because they're often stills */
    if( ti->duration < min_duration )
    {
        hb_log( "bd: ignoring title (too short)" );
        goto fail;
    }
    
    if ( max_duration > 0 && ti->duration > max_duration ) 
    {
        hb_log( "bd: ignoring title (too long)" );
        goto fail;
    }
    
    if (global_verbosity_level >= 2)
    {
        show_clip_list(ti);
    }

    BLURAY_STREAM_INFO * bdvideo = &ti->clips[0].video_streams[0];

    title->video_id = bdvideo->pid;
    title->video_stream_type = bdvideo->coding_type;

    hb_log( "bd: video id=0x%x, stream type=%s, format %s", title->video_id,
            bdvideo->coding_type == BLURAY_STREAM_TYPE_VIDEO_MPEG1 ? "MPEG1" :
            bdvideo->coding_type == BLURAY_STREAM_TYPE_VIDEO_MPEG2 ? "MPEG2" :
            bdvideo->coding_type == BLURAY_STREAM_TYPE_VIDEO_VC1 ? "VC-1" :
            bdvideo->coding_type == BLURAY_STREAM_TYPE_VIDEO_H264 ? "H.264" :
            bdvideo->coding_type == BLURAY_STREAM_TYPE_VIDEO_HEVC ? "HEVC" :
            "Unknown",
            bdvideo->format == BLURAY_VIDEO_FORMAT_480I ? "480i" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_576I ? "576i" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_480P ? "480p" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_1080I ? "1080i" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_720P ? "720p" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_1080P ? "1080p" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_576P ? "576p" :
            bdvideo->format == BLURAY_VIDEO_FORMAT_2160P ? "2160p" :
            "Unknown"
          );

    switch( bdvideo->coding_type )
    {
        case BLURAY_STREAM_TYPE_VIDEO_MPEG1:
        case BLURAY_STREAM_TYPE_VIDEO_MPEG2:
            title->video_codec = WORK_DECAVCODECV;
            title->video_codec_param = AV_CODEC_ID_MPEG2VIDEO;
            break;

        case BLURAY_STREAM_TYPE_VIDEO_VC1:
            title->video_codec = WORK_DECAVCODECV;
            title->video_codec_param = AV_CODEC_ID_VC1;
            break;

        case BLURAY_STREAM_TYPE_VIDEO_H264:
            title->video_codec = WORK_DECAVCODECV;
            title->video_codec_param = AV_CODEC_ID_H264;
            break;

        case BLURAY_STREAM_TYPE_VIDEO_HEVC:
            title->video_codec = WORK_DECAVCODECV;
            title->video_codec_param = AV_CODEC_ID_HEVC;
            break;

        default:
            hb_log( "scan: unknown video codec (0x%x)",
                    bdvideo->coding_type );
            goto fail;
    }

    switch ( bdvideo->aspect )
    {
        case BLURAY_ASPECT_RATIO_4_3:
            title->container_dar.num = 4;
            title->container_dar.den = 3;
            break;
        case BLURAY_ASPECT_RATIO_16_9:
            title->container_dar.num = 16;
            title->container_dar.den = 9;
            break;
        default:
            hb_log( "bd: unknown aspect %d, assuming 16:9", bdvideo->aspect );
            title->container_dar.num = 16;
            title->container_dar.den = 9;
            break;
    }
    hb_log("bd: aspect = %d:%d",
           title->container_dar.num, title->container_dar.den);

    /* Detect audio */
    // Max primary BD audios is 32
    int matches;
    int most_audio = 0;
    int audio_clip_index = 0;
    if (ti->clip_count > 2)
    {
        // All BD clips are not all required to have the same audio.
        // But clips that have seamless transition are required
        // to have the same audio as the previous clip.
        // So find the clip that has the most other clips with the
        // matching audio.
        for ( ii = 0; ii < ti->clip_count; ii++ )
        {
            matches = 0;
            for ( jj = 0; jj < ti->clip_count; jj++ )
            {
                if ( bd_audio_equal( &ti->clips[ii], &ti->clips[jj] ) )
                {
                    matches++;
                }
            }
            if ( matches > most_audio )
            {
                most_audio = matches;
                audio_clip_index = ii;
            }
        }
    }
    else if (ti->clip_count == 2)
    {
        // If there are only 2 clips, pick audios from the longer clip
        if (ti->clips[0].pkt_count < ti->clips[1].pkt_count)
            audio_clip_index = 1;
    }

    // Add all the audios found in the above clip.
    for (ii = 0; ii < ti->clips[audio_clip_index].audio_stream_count; ii++)
    {
        BLURAY_STREAM_INFO * bdaudio;
        int                  index;

        bdaudio = &ti->clips[audio_clip_index].audio_streams[ii];
        index   = hb_list_count(title->list_audio);

        switch (bdaudio->coding_type)
        {
            case BLURAY_STREAM_TYPE_AUDIO_TRUHD:
                // Add 2 audio tracks.  One for TrueHD and one for AC-3
                add_audio(index, index + 1, ii, title->list_audio, bdaudio, HB_SUBSTREAM_BD_AC3,
                          HB_ACODEC_AC3, AV_CODEC_ID_AC3, HB_AUDIO_ATTR_NONE);
                add_audio(index + 1, index, ii, title->list_audio, bdaudio, HB_SUBSTREAM_BD_TRUEHD,
                          HB_ACODEC_FFTRUEHD, AV_CODEC_ID_TRUEHD,
                          HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_DTS:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_DCA, AV_CODEC_ID_DTS, HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_MPEG2:
            case BLURAY_STREAM_TYPE_AUDIO_MPEG1:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFMPEG, AV_CODEC_ID_MP2,
                          HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_AC3PLUS:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFEAC3, AV_CODEC_ID_EAC3,
                          HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_AC3PLUS_SECONDARY:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFEAC3, AV_CODEC_ID_EAC3,
                          HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_LPCM:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFMPEG, AV_CODEC_ID_PCM_BLURAY,
                          HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_AC3:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_AC3, AV_CODEC_ID_AC3, HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_DTSHD_MASTER:
            case BLURAY_STREAM_TYPE_AUDIO_DTSHD:
                // Add 2 audio tracks.  One for DTS-HD and one for DTS
                add_audio(index, index + 1, ii, title->list_audio, bdaudio, HB_SUBSTREAM_BD_DTS,
                          HB_ACODEC_DCA, AV_CODEC_ID_DTS, HB_AUDIO_ATTR_NONE);
                // DTS-HD is special.  The substreams must be concatenated
                // DTS-core followed by DTS-hd-extensions.  Setting
                // a substream id of 0 says use all substreams.
                add_audio(index + 1, index, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_DCA_HD, AV_CODEC_ID_DTS,
                          HB_AUDIO_ATTR_NONE);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_DTSHD_SECONDARY:
                // BD "DTSHD_SECONDARY" is DTS Express which has no
                // DTS core
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_DCA_HD, AV_CODEC_ID_DTS,
                          HB_AUDIO_ATTR_NONE);
                break;

            default:
                hb_log("scan: unknown audio pid 0x%x codec 0x%x", bdaudio->pid,
                       bdaudio->coding_type);
                break;
        }
    }

    // Add all the secondary audios found in the above clip.
    for (jj = 0; jj < ti->clips[audio_clip_index].sec_audio_stream_count; jj++, ii++)
    {
        BLURAY_STREAM_INFO * bdaudio;
        int                  index;

        bdaudio = &ti->clips[audio_clip_index].sec_audio_streams[jj];
        index   = hb_list_count(title->list_audio);

        switch (bdaudio->coding_type)
        {
            case BLURAY_STREAM_TYPE_AUDIO_TRUHD:
                // Add 2 audio tracks.  One for TrueHD and one for AC-3
                add_audio(index, index + 1, ii, title->list_audio, bdaudio, HB_SUBSTREAM_BD_AC3,
                          HB_ACODEC_AC3, AV_CODEC_ID_AC3,
                          HB_AUDIO_ATTR_SECONDARY);
                add_audio(index + 1, index, ii, title->list_audio, bdaudio, HB_SUBSTREAM_BD_TRUEHD,
                          HB_ACODEC_FFTRUEHD, AV_CODEC_ID_TRUEHD,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_DTS:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_DCA, AV_CODEC_ID_DTS,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_MPEG2:
            case BLURAY_STREAM_TYPE_AUDIO_MPEG1:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFMPEG, AV_CODEC_ID_MP2,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_AC3PLUS:
            case BLURAY_STREAM_TYPE_AUDIO_AC3PLUS_SECONDARY:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFEAC3, AV_CODEC_ID_EAC3,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_LPCM:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_FFMPEG, AV_CODEC_ID_PCM_BLURAY,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_AC3:
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_AC3, AV_CODEC_ID_AC3,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_DTSHD_MASTER:
            case BLURAY_STREAM_TYPE_AUDIO_DTSHD:
                // Add 2 audio tracks.  One for DTS-HD and one for DTS
                add_audio(index, index + 1, ii, title->list_audio, bdaudio, HB_SUBSTREAM_BD_DTS,
                          HB_ACODEC_DCA, AV_CODEC_ID_DTS,
                          HB_AUDIO_ATTR_SECONDARY);
                // DTS-HD is special.  The substreams must be concatenated
                // DTS-core followed by DTS-hd-extensions.  Setting
                // a substream id of 0 says use all substreams.
                add_audio(index + 1, index, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_DCA_HD, AV_CODEC_ID_DTS,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            case BLURAY_STREAM_TYPE_AUDIO_DTSHD_SECONDARY:
                // BD "DTSHD_SECONDARY" is DTS Express which has no
                // DTS core
                add_audio(index, -1, ii, title->list_audio, bdaudio, 0,
                          HB_ACODEC_DCA_HD, AV_CODEC_ID_DTS,
                          HB_AUDIO_ATTR_SECONDARY);
                break;

            default:
                hb_log("scan: unknown audio pid 0x%x codec 0x%x", bdaudio->pid,
                       bdaudio->coding_type);
                break;
        }
    }

    // Add all the subtitles found in the above clip.
    for ( ii = 0; ii < ti->clips[audio_clip_index].pg_stream_count; ii++ )
    {
        BLURAY_STREAM_INFO * bdpgs;

        bdpgs = &ti->clips[audio_clip_index].pg_streams[ii];

        switch( bdpgs->coding_type )
        {
            case BLURAY_STREAM_TYPE_SUB_PG:
                add_subtitle(ii, title->list_subtitle, bdpgs, WORK_DECAVSUB,
                             AV_CODEC_ID_HDMV_PGS_SUBTITLE);
                break;
            default:
                hb_log( "scan: unknown subtitle pid 0x%x codec 0x%x",
                        bdpgs->pid, bdpgs->coding_type );
                break;
        }
    }

    /* Chapters */
    for ( ii = 0, jj = 0; ii < ti->chapter_count; ii++ )
    {
        char chapter_title[80];

        // Sanity check start time of this chapter.
        // If the chapter starts within 1.5 seconds of the end of
        // the title, drop it.
        if (ti->duration - ti->chapters[ii].start < 90000 * 1.5)
        {
            hb_log("bd: chapter %d too short %"PRIu64", dropping", ii+1,
                   ti->chapters[ii].start);
            continue;
        }

        chapter = calloc( sizeof( hb_chapter_t ), 1 );

        chapter->index = ++jj;
        snprintf( chapter_title, sizeof(chapter_title), "Chapter %d", chapter->index );
        hb_chapter_set_title( chapter, chapter_title );

        chapter->duration = ti->chapters[ii].duration;

        // Sanity check chapter duration and start times
        // Have seen some invalid durations in the wild
        if (ii < ti->chapter_count - 1)
        {
            // Validate start time
            if (ti->chapters[ii+1].start < ti->chapters[ii].start)
            {
                hb_log("bd: chapter %d invalid start %"PRIu64"", ii+1,
                       ti->chapters[ii+1].start);
                ti->chapters[ii+1].start = ti->chapters[ii].start +
                                           chapter->duration;
            }
            if (ti->chapters[ii+1].start - ti->chapters[ii].start !=
                chapter->duration)
            {
                hb_log("bd: chapter %d invalid duration %"PRIu64"", ii+1,
                       chapter->duration);
                chapter->duration = ti->chapters[ii+1].start -
                                    ti->chapters[ii].start;
            }
        }
        else
        {
            if (ti->duration - ti->chapters[ii].start != chapter->duration)
            {
                hb_log("bd: chapter %d invalid duration %"PRIu64"", ii+1,
                       chapter->duration);
                chapter->duration = ti->duration - ti->chapters[ii].start;
            }
        }

        int seconds      = ( chapter->duration + 45000 ) / 90000;
        chapter->hours   = ( seconds / 3600 );
        chapter->minutes = ( seconds % 3600 ) / 60;
        chapter->seconds = ( seconds % 60 );

        hb_log( "bd: chap %d, %"PRIu64" ms",
                chapter->index,
                chapter->duration / 90 );

        hb_list_add( title->list_chapter, chapter );
    }
    hb_log( "bd: title %d has %d chapters", tt, hb_list_count(title->list_chapter));

    /* This title is ok so far */
    goto cleanup;

fail:
    hb_title_close( &title );

cleanup:

    return title;
}

/***********************************************************************
 * hb_bd_main_feature
 **********************************************************************/
int hb_bd_main_feature( hb_bd_t * d, hb_list_t * list_title )
{
    int longest = 0;
    int ii;
    uint64_t longest_duration = 0;
    int highest_rank = 0, rank;
    int most_chapters = 0;
    int ranks[9] = {0, 1, 3, 2, 6, 5, 7, 4, 8};
    BLURAY_TITLE_INFO * ti;

    for ( ii = 0; ii < hb_list_count( list_title ); ii++ )
    {
        hb_title_t * title = hb_list_item( list_title, ii );
        ti = d->title_info[title->index - 1];
        if ( ti )
        {
            BLURAY_STREAM_INFO * bdvideo = &ti->clips[0].video_streams[0];
            if ( title->duration > longest_duration * 0.7 && bdvideo->format < 8 )
            {
                rank = 0;
                if (bdvideo->format <= 8)
                {
                    rank = ranks[bdvideo->format];
                }
                if (highest_rank < rank ||
                    ( title->duration > longest_duration &&
                      highest_rank == rank))
                {
                    longest = title->index;
                    longest_duration = title->duration;
                    highest_rank = rank;
                    most_chapters = ti->chapter_count;
                }
                else if (highest_rank == rank &&
                         title->duration == longest_duration &&
                         ti->chapter_count > most_chapters)
                {
                    longest = title->index;
                    most_chapters = ti->chapter_count;
                }
            }
        }
        else if ( title->duration > longest_duration )
        {
            longest_duration = title->duration;
            longest = title->index;
        }
    }
    return longest;
}

/***********************************************************************
 * hb_bd_start
 ***********************************************************************
 * Title and chapter start at 1
 **********************************************************************/
int hb_bd_start( hb_bd_t * d, hb_title_t *title )
{
    BD_EVENT event;

    d->duration  = title->duration;

    // Calling bd_get_event initializes libbluray event queue.
    bd_select_title( d->bd, d->title_info[title->index - 1]->idx );
    bd_get_event( d->bd, &event );
    d->chapter = 0;
    d->next_chap = 1;
    d->stream = hb_bd_stream_open( d->h, title );
    if ( d->stream == NULL )
    {
        return 0;
    }
    return 1;
}

/***********************************************************************
 * hb_bd_stop
 ***********************************************************************
 *
 **********************************************************************/
void hb_bd_stop( hb_bd_t * d )
{
    if( d->stream ) hb_stream_close( &d->stream );
}

/***********************************************************************
 * hb_bd_seek
 ***********************************************************************
 *
 **********************************************************************/
int hb_bd_seek( hb_bd_t * d, float f )
{
    uint64_t pos = f * d->duration;

    bd_seek_time(d->bd, pos);
    d->next_chap = bd_get_current_chapter( d->bd ) + 1;
    hb_ts_stream_reset(d->stream);
    return 1;
}

int hb_bd_seek_pts( hb_bd_t * d, uint64_t pts )
{
    bd_seek_time(d->bd, pts);
    d->next_chap = bd_get_current_chapter( d->bd ) + 1;
    hb_ts_stream_reset(d->stream);
    return 1;
}

int hb_bd_seek_chapter( hb_bd_t * d, int c )
{
    d->next_chap = c;
    bd_seek_chapter( d->bd, c - 1 );
    hb_ts_stream_reset(d->stream);
    return 1;
}

/***********************************************************************
 * hb_bd_read
 ***********************************************************************
 *
 **********************************************************************/
hb_buffer_t * hb_bd_read( hb_bd_t * d )
{
    int result;
    int error_count = 0;
    int retry_count = 0;
    uint8_t buf[192];
    BD_EVENT event;
    uint64_t pos;
    hb_buffer_t * out = NULL;
    uint8_t discontinuity;

    while ( 1 )
    {
        discontinuity = 0;
        result = next_packet( d->bd, buf );
        while ( bd_get_event( d->bd, &event ) )
        {
            switch ( event.event )
            {
                case BD_EVENT_CHAPTER:
                    // The muxers expect to only get chapter 2 and above
                    // They write chapter 1 when chapter 2 is detected.
                    if (event.param > d->chapter)
                    {
                        d->next_chap = event.param;
                    }
                    break;

                case BD_EVENT_PLAYITEM:
                    discontinuity = 1;
                    hb_deep_log(2, "bd: Play item %u", event.param);
                    break;

                case BD_EVENT_STILL:
                    bd_read_skip_still( d->bd );
                    break;

                case BD_EVENT_END_OF_TITLE:
                    hb_log("bd: End of title");
                    if (result <= 0)
                    {
                        return NULL;
                    }
                    break;

                default:
                    break;
            }
        }

        if ( result < 0 )
        {
            hb_error("bd: Read Error");
            pos = bd_tell( d->bd );
            bd_seek( d->bd, pos + 192 );
            error_count++;
            if (error_count > 10)
            {
                hb_error("bd: Error, too many consecutive read errors");
                hb_set_work_error(d->h, HB_ERROR_READ);
                return NULL;
            }
            continue;
        }
        else if ( result == 0 )
        {
            // libbluray returns 0 when it encounters and skips a bad unit.
            // So retry a few times to be certain there is no more data
            // to be read.
            retry_count++;
            if (retry_count > 1000)
            {
                // A unit is 6144 bytes (32 TS packets).  Give up after we've
                // seen > 6MB of invalid data.
                hb_error("bd: Error, too many consecutive bad units.");
                hb_set_work_error(d->h, HB_ERROR_READ);
                return NULL;
            }
            continue;
        }

        if (retry_count > 0)
        {
            hb_error("bd: Read Error, skipping bad data.");
            retry_count = 0;
        }

        error_count = 0;
        // buf+4 to skip the BD timestamp at start of packet
        if (d->chapter != d->next_chap)
        {
            d->chapter = d->next_chap;
            out = hb_ts_decode_pkt(d->stream, buf+4, d->chapter, discontinuity);
        }
        else
        {
            out = hb_ts_decode_pkt(d->stream, buf+4, 0, discontinuity);
        }
        if (out != NULL)
        {
            return out;
        }
    }
}

/***********************************************************************
 * hb_bd_chapter
 ***********************************************************************
 * Returns in which chapter the next block to be read is.
 * Chapter numbers start at 1.
 **********************************************************************/
int hb_bd_chapter( hb_bd_t * d )
{
    return d->chapter;
}

/***********************************************************************
 * hb_bd_close
 ***********************************************************************
 * Closes and frees everything
 **********************************************************************/
void hb_bd_close( hb_bd_t ** _d )
{
    hb_bd_t * d = *_d;
    int ii;

    if ( d->title_info )
    {
        for ( ii = 0; ii < d->title_count; ii++ )
            bd_free_title_info( d->title_info[ii] );
        free( d->title_info );
    }
    if( d->stream ) hb_stream_close( &d->stream );
    if( d->bd ) bd_close( d->bd );
    if( d->path ) free( d->path );

    free( d );
    *_d = NULL;
}

/***********************************************************************
 * hb_bd_set_angle
 ***********************************************************************
 * Sets the angle to read
 **********************************************************************/
void hb_bd_set_angle( hb_bd_t * d, int angle )
{

    if ( !bd_select_angle( d->bd, angle) )
    {
        hb_log("bd_select_angle failed");
    }
}

static int check_ts_sync(const uint8_t *buf)
{
    // must have initial sync byte, no scrambling & a legal adaptation ctrl
    return (buf[0] == 0x47) && ((buf[3] >> 6) == 0) && ((buf[3] >> 4) > 0);
}

static int have_ts_sync(const uint8_t *buf, int psize)
{
    return check_ts_sync(&buf[0*psize]) && check_ts_sync(&buf[1*psize]) &&
           check_ts_sync(&buf[2*psize]) && check_ts_sync(&buf[3*psize]) &&
           check_ts_sync(&buf[4*psize]) && check_ts_sync(&buf[5*psize]) &&
           check_ts_sync(&buf[6*psize]) && check_ts_sync(&buf[7*psize]);
}

#define MAX_HOLE 192*80

static uint64_t align_to_next_packet(BLURAY *bd, uint8_t *pkt)
{
    int      result;
    uint8_t  buf[MAX_HOLE];
    uint64_t pos = 0;
    uint64_t start = bd_tell(bd);
    uint64_t orig;
    uint64_t off = 192;

    memcpy(buf, pkt, 192);
    if ( start >= 192 ) {
        start -= 192;
    }
    orig = start;

    while (1)
    {
        result = bd_read(bd, buf + off, sizeof(buf) - off);
        if (result == sizeof(buf) - off)
        {
            const uint8_t *bp = buf;
            int i;

            for ( i = sizeof(buf) - 8 * 192; --i >= 0; ++bp )
            {
                if ( have_ts_sync( bp, 192 ) )
                {
                    break;
                }
            }
            if ( i >= 0 )
            {
                pos = ( bp - buf );
                break;
            }
            off = 8 * 192;
            memcpy(buf, buf + sizeof(buf) - off, off);
            start += sizeof(buf) - off;
        }
        else if (result < 0)
        {
            return -1;
        }
        else
        {
            return 0;
        }
    }
    off = start + pos - 4;
    // bd_seek seeks to the nearest access unit *before* the requested position
    // we don't want to seek backwards, so we need to read until we get
    // past that position.
    bd_seek(bd, off);
    while (off > bd_tell(bd))
    {
        result = bd_read(bd, buf, 192);
        if (result < 0)
        {
            return -1;
        }
        else if (result != 192)
        {
            return 0;
        }
    }
    return start - orig + pos;
}

static int next_packet( BLURAY *bd, uint8_t *pkt )
{
    int result;

    while ( 1 )
    {
        result = bd_read( bd, pkt, 192 );
        if ( result < 0 )
        {
            return -1;
        }
        if ( result < 192 )
        {
            return 0;
        }
        // Sync byte is byte 4.  0-3 are timestamp.
        if (pkt[4] == 0x47)
        {
            return 1;
        }
        // lost sync - back up to where we started then try to re-establish.
        uint64_t pos = bd_tell(bd);
        uint64_t pos2 = align_to_next_packet(bd, pkt);
        if (pos2 < 0)
        {
            return -1;
        }
        else if (pos2 == 0)
        {
            hb_log("next_packet: eof while re-establishing sync @ %"PRIu64"", pos );
            return 0;
        }
        hb_log("next_packet: sync lost @ %"PRIu64", regained after %"PRIu64" bytes",
                 pos, pos2 );
    }
}

static int title_info_compare_mpls(const void *va, const void *vb)
{
    BLURAY_TITLE_INFO *a, *b;

    a = *(BLURAY_TITLE_INFO**)va;
    b = *(BLURAY_TITLE_INFO**)vb;

    return a->playlist - b->playlist;
}
