diff mbox series

[FFmpeg-devel,v3] avformat/dvd: new dvd:// protocol for reading dvd folder/images

Message ID 20230108150622.15075-1-stanislav.ionascu@gmail.com
State New
Headers show
Series [FFmpeg-devel,v3] avformat/dvd: new dvd:// protocol for reading dvd folder/images | expand

Checks

Context Check Description
yinshiyou/make_loongarch64 success Make finished
yinshiyou/make_fate_loongarch64 success Make fate finished
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Stanislav Ionascu Jan. 8, 2023, 3:06 p.m. UTC
dvd:// protocol uses libdvdread for opening folders and/or disc-images,
it then either identifies the longest title-set, or uses the title-set
parameter.

After opening the dvd, it will read and output all VOBs, in sequence
defined by the program-chain.

Signed-off-by: Stanislav Ionascu <stanislav.ionascu@gmail.com>
---
 configure               |   5 +
 libavformat/Makefile    |   1 +
 libavformat/dvd.c       | 327 ++++++++++++++++++++++++++++++++++++++++
 libavformat/protocols.c |   1 +
 4 files changed, 334 insertions(+)
 create mode 100644 libavformat/dvd.c

Comments

Nicolas George Jan. 9, 2023, 9:12 a.m. UTC | #1
Stanislav Ionascu (12023-01-08):
> dvd:// protocol uses libdvdread for opening folders and/or disc-images,
> it then either identifies the longest title-set, or uses the title-set
> parameter.
> 
> After opening the dvd, it will read and output all VOBs, in sequence
> defined by the program-chain.

Thanks for the patch. The reason there was no DVD support in ffmpeg
until now is that it is not as straightforward as it seems.

Your patch is missing documentation, including documentation about its
limitations. Please clarify various points:

- VOBs contain not only the movie but also menu data: will it be mixed
  along with the movie or skipped?

- Are the timestamps monotonous over a whole title?

- Does ffmpeg correctly detect all the subtitles streams?

- Does ffmpeg correctly detect the language of audio and subtitles
  streams?

- Are chapters detected?

Regards,
Stanislav Ionascu Jan. 9, 2023, 10:56 a.m. UTC | #2
Thanks for looking into this.

On Mon, Jan 9, 2023 at 9:13 AM Nicolas George <george@nsup.org> wrote:
>
> Stanislav Ionascu (12023-01-08):
> > dvd:// protocol uses libdvdread for opening folders and/or disc-images,
> > it then either identifies the longest title-set, or uses the title-set
> > parameter.
> >
> > After opening the dvd, it will read and output all VOBs, in sequence
> > defined by the program-chain.
>
> Thanks for the patch. The reason there was no DVD support in ffmpeg
> until now is that it is not as straightforward as it seems.
>
> Your patch is missing documentation, including documentation about its
> limitations. Please clarify various points:
>
> - VOBs contain not only the movie but also menu data: will it be mixed
>   along with the movie or skipped?

The protocol, when opening the DVD folder or image, will read the VMG
and VTS info to identify the longest title. If identification was
incorrect, the title can be overridden via command-line parameters.
But most of the time it will be the main-movie. The VOBs that are read
via libdvdread belong to that video title set, so menu VOBs should be
skipped.

>
> - Are the timestamps monotonous over a whole title?

Since this is a protocol, it doesn't really do much about demuxing and
the timestamps, so it depends on the data in VOBs and the probed
demuxer (mpeg). From the ffmpeg perspective, this is a continuous
single concatenated VOB, similar to what the "concat" demuxer
generates from split-VOBs, so generating timestamps may be required
when muxing into a format that requires them, like matroska.

>
> - Does ffmpeg correctly detect all the subtitles streams?

Yes, ffmpeg either discovers them later in the stream, or with
adequate analyzeduration/probesize during probing.

>
> - Does ffmpeg correctly detect the language of audio and subtitles
>   streams?
>
> - Are chapters detected?

As this is protocol, it only provides the data that is stored in the
VOBs. Libdvdread has the language and chapter information, but I don't
know how to attach it to the stream without having to use a slave
mpeg-demuxer.

For the sample files I've used :
1. streams.videolan.org/samples/MPEG-VOB/menus\ DVD/free.iso
2. www.deniscarl.com/dvdtest/videotest.iso.bz2
3. dvdauthor-built disc from generated av streams

>
> Regards,
>
> --
>   Nicolas George

Thanks!

Stan.
diff mbox series

Patch

diff --git a/configure b/configure
index 870d426b0e..312144c105 100755
--- a/configure
+++ b/configure
@@ -230,6 +230,7 @@  External library support:
   --enable-libdavs2        enable AVS2 decoding via libdavs2 [no]
   --enable-libdc1394       enable IIDC-1394 grabbing using libdc1394
                            and libraw1394 [no]
+  --enable-libdvdread      enable DVD reading using libdvdread [no]
   --enable-libfdk-aac      enable AAC de/encoding via libfdk-aac [no]
   --enable-libflite        enable flite (voice synthesis) support via libflite [no]
   --enable-libfontconfig   enable libfontconfig, useful for drawtext filter [no]
@@ -1814,6 +1815,7 @@  EXTERNAL_LIBRARY_LIST="
     libdav1d
     libdc1394
     libdrm
+    libdvdread
     libflite
     libfontconfig
     libfreetype
@@ -3557,6 +3559,7 @@  xv_outdev_deps="xlib_xv xlib_x11 xlib_xext"
 # protocols
 async_protocol_deps="threads"
 bluray_protocol_deps="libbluray"
+dvd_protocol_deps="libdvdread"
 ffrtmpcrypt_protocol_conflict="librtmp_protocol"
 ffrtmpcrypt_protocol_deps_any="gcrypt gmp openssl mbedtls"
 ffrtmpcrypt_protocol_select="tcp_protocol"
@@ -6568,6 +6571,8 @@  enabled libdav1d          && require_pkg_config libdav1d "dav1d >= 0.5.0" "dav1d
 enabled libdavs2          && require_pkg_config libdavs2 "davs2 >= 1.6.0" davs2.h davs2_decoder_open
 enabled libdc1394         && require_pkg_config libdc1394 libdc1394-2 dc1394/dc1394.h dc1394_new
 enabled libdrm            && require_pkg_config libdrm libdrm xf86drm.h drmGetVersion
+enabled libdvdread        && enable dvdread
+enabled dvdread           && require_pkg_config libdvdread dvdread "dvdread/dvd_reader.h" DVDOpen2
 enabled libfdk_aac        && { check_pkg_config libfdk_aac fdk-aac "fdk-aac/aacenc_lib.h" aacEncOpen ||
                                { require libfdk_aac fdk-aac/aacenc_lib.h aacEncOpen -lfdk-aac &&
                                  warn "using libfdk without pkg-config"; } }
diff --git a/libavformat/Makefile b/libavformat/Makefile
index d7f198bf39..a31fafcadb 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -644,6 +644,7 @@  OBJS-$(CONFIG_CONCAT_PROTOCOL)           += concat.o
 OBJS-$(CONFIG_CONCATF_PROTOCOL)          += concat.o
 OBJS-$(CONFIG_CRYPTO_PROTOCOL)           += crypto.o
 OBJS-$(CONFIG_DATA_PROTOCOL)             += data_uri.o
+OBJS-$(CONFIG_DVD_PROTOCOL)              += dvd.o
 OBJS-$(CONFIG_FFRTMPCRYPT_PROTOCOL)      += rtmpcrypt.o rtmpdigest.o rtmpdh.o
 OBJS-$(CONFIG_FFRTMPHTTP_PROTOCOL)       += rtmphttp.o
 OBJS-$(CONFIG_FILE_PROTOCOL)             += file.o
diff --git a/libavformat/dvd.c b/libavformat/dvd.c
new file mode 100644
index 0000000000..7a70189c69
--- /dev/null
+++ b/libavformat/dvd.c
@@ -0,0 +1,327 @@ 
+/*
+ * DVD (libdvdread) protocol
+ * Copyright (c) 2023 Stan Ionascu <stanislav.ionascu@gmail.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <dvdread/dvd_reader.h>
+#include <dvdread/ifo_read.h>
+#include <dvdread/ifo_print.h>
+
+#include "libavformat/avformat.h"
+#include "libavformat/url.h"
+#include "libavutil/avstring.h"
+#include "libavutil/opt.h"
+
+#define DVD_PROTO_PREFIX "dvd:"
+#define DVD_SECTOR_SIZE 2048
+
+typedef struct {
+    const AVClass *class;
+
+    dvd_reader_t *dvd;
+    dvd_logger_cb dvd_logger;
+
+    int title_nr;
+    int angle_nr;
+    dvd_file_t *dvd_title_file;
+    ifo_handle_t *vmg_ifo;
+    ifo_handle_t *vts_ifo;
+    pgc_t *current_pgc;
+    int current_cell;
+
+    int start_sector;
+    int current_sector;
+    int end_sector;
+
+} DvdProtocolContext;
+
+static int bcd2int(int bcd)
+{
+    return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f));
+}
+
+static void dvd_logger_ff(void *opaque, dvd_logger_level_t dvd_level, const char *fmt, va_list args)
+{
+    int level = AV_LOG_TRACE;
+    if (dvd_level == DVD_LOGGER_LEVEL_ERROR) {
+        level = AV_LOG_ERROR;
+    } else if (dvd_level == DVD_LOGGER_LEVEL_WARN) {
+        level = AV_LOG_WARNING;
+    } else if (dvd_level == DVD_LOGGER_LEVEL_INFO) {
+        level = AV_LOG_INFO;
+    } else if (dvd_level == DVD_LOGGER_LEVEL_DEBUG) {
+        level = AV_LOG_DEBUG;
+    }
+    av_vlog(opaque, level, fmt, args);
+    /* add new line */
+    av_log(opaque, level, "\n");
+}
+
+static int ff_dvd_get_title_set_length(ifo_handle_t *vts_ifo, tt_srpt_t *tt_srpt, int title_nr)
+{
+    /* reindex 1 => 0 */
+    int vts_title_number = tt_srpt->title[title_nr].vts_ttn - 1;
+    int pgc_number = vts_ifo->vts_ptt_srpt->title[vts_title_number].ptt[0].pgcn;
+    dvd_time_t *playback_time = &vts_ifo->vts_pgcit->pgci_srp[pgc_number - 1].pgc->playback_time;
+
+    return (bcd2int(playback_time->hour) * 3600 +
+            bcd2int(playback_time->minute) * 60 +
+            bcd2int(playback_time->second)) * 1000;
+}
+
+static void ff_dvd_set_program_chain_info(DvdProtocolContext *ctx, int title_nr, int ptt_nr)
+{
+    /* reindex 1 => 0 */
+    int vts_title_number = ctx->vmg_ifo->tt_srpt->title[title_nr].vts_ttn - 1;
+    ptt_info_t *ptt = &ctx->vts_ifo->vts_ptt_srpt->title[vts_title_number].ptt[ptt_nr];
+    int pgc_id = ptt->pgcn;
+    int pgn  = ptt->pgn;
+
+    ctx->current_pgc = ctx->vts_ifo->vts_pgcit->pgci_srp[pgc_id - 1].pgc;
+    ctx->current_cell = ctx->current_pgc->program_map[pgn - 1] - 1;
+
+    /* change angle */
+    if(ctx->current_pgc->cell_playback[ctx->current_cell].block_type == BLOCK_TYPE_ANGLE_BLOCK ) {
+      ctx->current_cell += ctx->angle_nr;
+    }
+
+    ctx->current_sector = ctx->start_sector = ctx->current_pgc->cell_playback[ctx->current_cell].first_sector;
+    ctx->end_sector = ctx->current_pgc->cell_playback[ctx->current_cell].last_sector;
+}
+
+static int ff_dvd_get_next_cell(DvdProtocolContext *ctx)
+{
+    int next_cell = ctx->current_cell;
+
+    /* fast-forward until last-cell */
+    if (ctx->current_pgc->cell_playback[next_cell].block_type == BLOCK_TYPE_ANGLE_BLOCK) {
+        ctx->current_cell += ctx->angle_nr;
+        while (next_cell < ctx->current_pgc->nr_of_cells &&
+                ctx->current_pgc->cell_playback[next_cell].block_type != BLOCK_MODE_LAST_CELL) {
+            next_cell++;
+        }
+    }
+
+    /* take next cell */
+    next_cell++;
+    if (next_cell >= ctx->current_pgc->nr_of_cells) {
+        return -1;
+    }
+
+    return next_cell;
+}
+
+static int dvd_url_close(URLContext *h)
+{
+    DvdProtocolContext *ctx = h->priv_data;
+    ifoClose(ctx->vts_ifo);
+    ifoClose(ctx->vmg_ifo);
+    DVDCloseFile(ctx->dvd_title_file);
+    DVDClose(ctx->dvd);
+    return 0;
+}
+
+static int dvd_url_open(URLContext *h, const char *path, int flags)
+{
+    DvdProtocolContext *ctx = h->priv_data;
+    const char *dvd_path = path;
+    ifo_handle_t *vmg_ifo, *vts_ifo;
+    tt_srpt_t *tt_srpt;
+    int num_vts, num_titles;
+    int longest_title_nr;
+    int64_t longest_title_length_ms = 0;
+    int vts_nr;
+    int title_set_nr;
+    char disc_volume_id[32];
+
+    ctx->vmg_ifo = ctx->vts_ifo = NULL;
+    ctx->dvd_logger.pf_log = &dvd_logger_ff;
+
+    /* strip protocol from path */
+    av_strstart(path, DVD_PROTO_PREFIX, &dvd_path);
+
+    /* open dvd folder or disc */
+    if ((ctx->dvd = DVDOpen2(h, &ctx->dvd_logger, dvd_path)) == NULL) {
+        av_log(h, AV_LOG_ERROR, "DVDOpen(%s) failed\n", dvd_path);
+        return AVERROR(EIO);
+    }
+
+    if (DVDUDFVolumeInfo(ctx->dvd, &disc_volume_id[0], 32, NULL, 0) == 0) {
+        av_log(h, AV_LOG_INFO, "opened disc-volume-id: %s\n", disc_volume_id);
+    }
+
+    /* read toc */
+    if ((vmg_ifo = ifoOpen(ctx->dvd, 0)) == NULL) {
+        av_log(h, AV_LOG_ERROR, "ifoOpen(0) failed\n");
+        return AVERROR(EIO);
+    }
+
+    /* read titles */
+    num_vts = vmg_ifo->vts_atrt->nr_of_vtss;
+    num_titles = vmg_ifo->tt_srpt->nr_of_srpts;
+    av_log(h, AV_LOG_INFO, "there are %d usable titles\n", num_titles);
+
+    /* get title-set info(s) */
+    tt_srpt = vmg_ifo->tt_srpt;
+    if (ctx->title_nr >= num_titles) {
+        av_log(h, AV_LOG_ERROR, "invalid title id %d\n", ctx->title_nr);
+        return AVERROR(EINVAL);
+    }
+
+    for (vts_nr = 1; vts_nr <= num_vts; vts_nr++) {
+        int title_nr;
+        for (title_nr = 0; title_nr < num_titles; title_nr++) {
+            int title_length_ms;
+
+            if (tt_srpt->title[title_nr].title_set_nr != vts_nr) {
+                continue;
+            }
+
+            /* describe the title(s) info */
+            if ((vts_ifo = ifoOpen(ctx->dvd, vts_nr)) == NULL) {
+                av_log(h, AV_LOG_ERROR, "ifoOpen(%d) failed\n", title_nr);
+                return AVERROR(ENOMEM);
+            }
+
+            /* skip if video title set or program chain info is missing */
+            if (vts_ifo->vtsi_mat == NULL || vts_ifo->vts_pgcit == NULL) {
+                ifoClose(vts_ifo);
+                av_log(h, AV_LOG_TRACE, "skip title %d as no vts or pgc info is present\n",
+                        title_nr);
+                continue;
+            }
+
+            /* skip if vts_ttn is incorrect */
+            if (tt_srpt->title[title_nr].vts_ttn < 1 ||
+                tt_srpt->title[title_nr].vts_ttn > num_titles) {
+                ifoClose(vts_ifo);
+                av_log(h, AV_LOG_WARNING, "skip title %d as vts_ttn is out of bounds\n",
+                        title_nr);
+                continue;
+            }
+
+            title_length_ms = ff_dvd_get_title_set_length(vts_ifo, tt_srpt, title_nr);
+            av_log(h, AV_LOG_INFO, "title %03d : (%d:%02d:%02d) and %d chapter(s)\n",
+                title_nr,
+                (title_length_ms / 3600000),
+                (title_length_ms % 3600000) / 60000,
+                (title_length_ms % 60000) / 1000,
+                tt_srpt->title[title_nr].nr_of_ptts);
+
+            if (longest_title_length_ms <= title_length_ms) {
+                longest_title_nr = title_nr;
+                longest_title_length_ms = title_length_ms;
+            }
+
+            ifoClose(vts_ifo);
+        }
+    }
+
+    if (ctx->title_nr < 0) {
+        ctx->title_nr = longest_title_nr;
+    }
+
+    /* check if title selection is valid */
+    if (tt_srpt->title[ctx->title_nr].vts_ttn < 1 ||
+        tt_srpt->title[ctx->title_nr].vts_ttn > num_titles) {
+        av_log(h, AV_LOG_INFO, "selected title %d is not valid, vts_ttn is out of bounds\n",
+                ctx->title_nr);
+        ifoClose(vmg_ifo);
+        return AVERROR(EIO);
+    }
+
+    av_log(h, AV_LOG_INFO, "selected title %d\n", ctx->title_nr);
+
+    if (ctx->angle_nr > 0 && ctx->angle_nr >= tt_srpt->title[ctx->title_nr].nr_of_angles) {
+        av_log(h, AV_LOG_ERROR, "incorrect angle selected %d out of %d angle(s)\n",
+                ctx->angle_nr,
+                tt_srpt->title[ctx->title_nr].nr_of_angles);
+    }
+
+    /* open request title_set info */
+    title_set_nr = tt_srpt->title[ctx->title_nr].title_set_nr;
+    if ((vts_ifo = ifoOpen(ctx->dvd, title_set_nr)) == NULL) {
+        av_log(h, AV_LOG_INFO, "ifoOpen(%d) failed\n", title_set_nr);
+        return AVERROR(ENOMEM);
+    }
+
+    /* open file */
+    if ((ctx->dvd_title_file = DVDOpenFile(ctx->dvd, title_set_nr, DVD_READ_TITLE_VOBS)) == NULL) {
+        av_log(h, AV_LOG_ERROR, "DVDOpenFile(%d) failed\n", title_set_nr);
+        return AVERROR(EIO);
+    }
+
+    ctx->vmg_ifo = vmg_ifo;
+    ctx->vts_ifo = vts_ifo;
+
+    ff_dvd_set_program_chain_info(ctx, ctx->title_nr, 0);
+
+    return 0;
+}
+
+static int dvd_url_read(URLContext *h, unsigned char *buf, int size)
+{
+    DvdProtocolContext *ctx = h->priv_data;
+    ssize_t blocks_got;
+    int num_blocks = size / DVD_SECTOR_SIZE;
+
+    if (ctx->current_sector >= ctx->end_sector) {
+        int next_cell = ff_dvd_get_next_cell(ctx);
+        if (next_cell < 0) {
+            return AVERROR_EOF;
+        }
+
+        ctx->current_cell = next_cell;
+        ctx->start_sector = ctx->current_pgc->cell_playback[ctx->current_cell].first_sector;
+        ctx->end_sector = ctx->current_pgc->cell_playback[ctx->current_cell].last_sector;
+        ctx->current_sector = ctx->start_sector;
+    }
+
+    /* read all sectors that fit into buf */
+    blocks_got = DVDReadBlocks(ctx->dvd_title_file, ctx->current_sector, num_blocks, buf);
+    if (blocks_got <= 0) {
+        av_log(h, AV_LOG_ERROR, "failed to DVDReadBlocks() %d blocks at offset %d\n",
+                num_blocks, ctx->current_sector);
+        return AVERROR(EIO);
+    }
+    ctx->current_sector += blocks_got;
+    return blocks_got <= 0 ? AVERROR_EOF : blocks_got * DVD_SECTOR_SIZE;
+}
+
+#define OFFSET(x) offsetof(DvdProtocolContext, x)
+static const AVOption options[] = {
+    {"title", "", OFFSET(title_nr), AV_OPT_TYPE_INT, { .i64=-1 }, -1,  9999, AV_OPT_FLAG_DECODING_PARAM },
+    {"angle", "", OFFSET(angle_nr), AV_OPT_TYPE_INT, { .i64=0 },   0,   256, AV_OPT_FLAG_DECODING_PARAM },
+    { NULL },
+};
+static const AVClass dvd_context_class = {
+    .class_name     = "dvd",
+    .item_name      = av_default_item_name,
+    .option         = options,
+    .version        = LIBAVUTIL_VERSION_INT,
+};
+
+const URLProtocol ff_dvd_protocol = {
+    .name            = "dvd",
+    .url_close       = dvd_url_close,
+    .url_open        = dvd_url_open,
+    .url_read        = dvd_url_read,
+    .priv_data_size  = sizeof(DvdProtocolContext),
+    .priv_data_class = &dvd_context_class,
+};
diff --git a/libavformat/protocols.c b/libavformat/protocols.c
index 8b7d1b940f..69f015c532 100644
--- a/libavformat/protocols.c
+++ b/libavformat/protocols.c
@@ -29,6 +29,7 @@  extern const URLProtocol ff_concat_protocol;
 extern const URLProtocol ff_concatf_protocol;
 extern const URLProtocol ff_crypto_protocol;
 extern const URLProtocol ff_data_protocol;
+extern const URLProtocol ff_dvd_protocol;
 extern const URLProtocol ff_ffrtmpcrypt_protocol;
 extern const URLProtocol ff_ffrtmphttp_protocol;
 extern const URLProtocol ff_file_protocol;