1 /*
2 mkvmerge -- utility for splicing together matroska files
3 from component media subtypes
4
5 Distributed under the GPL v2
6 see the file COPYING for details
7 or visit https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
8
9 helper functions for chapters on DVDs
10
11 Written by Moritz Bunkus <moritz@bunkus.org>.
12 */
13
14 #include "common/common_pch.h"
15
16 #if defined(HAVE_DVDREAD)
17
18 #include <dvdread/dvd_reader.h>
19 #include <dvdread/ifo_read.h>
20 #include <dvdread/ifo_print.h>
21
22 #include <QRegularExpression>
23
24 #include "common/at_scope_exit.h"
25 #include "common/chapters/chapters.h"
26 #include "common/chapters/dvd.h"
27 #include "common/path.h"
28 #include "common/qt.h"
29 #include "common/strings/parsing.h"
30 #include "common/timestamp.h"
31
32 namespace mtx::chapters {
33
34 namespace {
35
36 timestamp_c
frames_to_timestamp_ns(unsigned int num_frames,unsigned int fps)37 frames_to_timestamp_ns(unsigned int num_frames,
38 unsigned int fps) {
39 auto factor = fps == 30 ? 1001 : 1000;
40 return timestamp_c::ns(1000000ull * factor * num_frames / (fps ? fps : 1));
41 }
42
43 } // anonymous namespace
44
45
46 std::vector<std::vector<timestamp_c>>
parse_dvd(std::string const & file_name)47 parse_dvd(std::string const &file_name) {
48 dvd_reader_t *dvd{};
49 ifo_handle_t *vmg{};
50
51 at_scope_exit_c global_cleanup{[dvd, vmg]() {
52 if (vmg)
53 ifoClose(vmg);
54 if (dvd)
55 DVDClose(dvd);
56 }};
57
58 auto error = fmt::format(Y("Could not open '{0}' for reading.\n"), file_name);
59
60 dvd = DVDOpen(file_name.c_str());
61 if (!dvd)
62 throw parser_x{error};
63
64 vmg = ifoOpen(dvd, 0);
65 if (!vmg)
66 throw parser_x{error};
67
68 std::vector<std::vector<timestamp_c>> titles_and_timestamps;
69 titles_and_timestamps.reserve(vmg->tt_srpt->nr_of_srpts);
70
71 for (auto title = 0; title < vmg->tt_srpt->nr_of_srpts; ++title) {
72 auto vts = ifoOpen(dvd, vmg->tt_srpt->title[title].title_set_nr);
73
74 at_scope_exit_c title_cleanup{[vts]() {
75 if (vts)
76 ifoClose(vts);
77 }};
78
79 if (!vts)
80 throw parser_x{error};
81
82 titles_and_timestamps.emplace_back();
83
84 auto ×tamps = titles_and_timestamps.back();
85 auto ttn = vmg->tt_srpt->title[title].vts_ttn;
86 auto vts_ptt_srpt = vts->vts_ptt_srpt;
87 auto overall_frames = 0u;
88 auto fps = 0u; // This should be consistent as DVDs are either NTSC or PAL
89
90 for (auto chapter = 0; chapter < vmg->tt_srpt->title[title].nr_of_ptts - 1; chapter++) {
91 auto pgc_id = vts_ptt_srpt->title[ttn - 1].ptt[chapter].pgcn;
92 auto pgn = vts_ptt_srpt->title[ttn - 1].ptt[chapter].pgn;
93 auto cur_pgc = vts->vts_pgcit->pgci_srp[pgc_id - 1].pgc;
94 auto start_cell = cur_pgc->program_map[pgn - 1] - 1;
95 pgc_id = vts_ptt_srpt->title[ttn - 1].ptt[chapter + 1].pgcn;
96 pgn = vts_ptt_srpt->title[ttn - 1].ptt[chapter + 1].pgn;
97 cur_pgc = vts->vts_pgcit->pgci_srp[pgc_id - 1].pgc;
98 auto end_cell = cur_pgc->program_map[pgn - 1] - 2;
99 auto cur_frames = 0u;
100
101 for (auto cur_cell = start_cell; cur_cell <= end_cell; cur_cell++) {
102 auto dt = &cur_pgc->cell_playback[cur_cell].playback_time;
103 auto hour = ((dt->hour & 0xf0) >> 4) * 10 + (dt->hour & 0x0f);
104 auto minute = ((dt->minute & 0xf0) >> 4) * 10 + (dt->minute & 0x0f);
105 auto second = ((dt->second & 0xf0) >> 4) * 10 + (dt->second & 0x0f);
106 fps = ((dt->frame_u & 0xc0) >> 6) == 1 ? 25 : 30; // by definition
107 cur_frames += ((hour * 60 * 60) + minute * 60 + second) * fps;
108 cur_frames += ((dt->frame_u & 0x30) >> 4) * 10 + (dt->frame_u & 0x0f);
109 }
110
111 timestamps.emplace_back(frames_to_timestamp_ns(overall_frames, fps));
112
113 overall_frames += cur_frames;
114 }
115
116 timestamps.emplace_back(frames_to_timestamp_ns(overall_frames, fps));
117 }
118
119 return titles_and_timestamps;
120 }
121
122 std::shared_ptr<libmatroska::KaxChapters>
maybe_parse_dvd(std::string const & file_name,mtx::bcp47::language_c const & language)123 maybe_parse_dvd(std::string const &file_name,
124 mtx::bcp47::language_c const &language) {
125 auto title = 1u;
126 auto cleaned_file_name = file_name;
127 auto matches = QRegularExpression{"(.+):([0-9]+)$"}.match(Q(cleaned_file_name));
128
129 if (matches.hasMatch()) {
130 cleaned_file_name = to_utf8(matches.captured(1));
131
132 if (!mtx::string::parse_number(to_utf8(matches.captured(2)), title) || (title < 1))
133 throw parser_x{fmt::format(Y("'{0}' is not a valid DVD title number."), to_utf8(matches.captured(2)))};
134 }
135
136 auto dvd_dir = mtx::fs::to_path(cleaned_file_name);
137
138 if (Q(cleaned_file_name).contains(QRegularExpression{"\\.(bup|ifo|vob)$", QRegularExpression::CaseInsensitiveOption}))
139 dvd_dir = dvd_dir.parent_path();
140
141 else if ( !std::filesystem::is_directory(dvd_dir)
142 || ( !std::filesystem::is_regular_file(dvd_dir / "VIDEO_TS.IFO")
143 && !std::filesystem::is_regular_file(dvd_dir / "VIDEO_TS" / "VIDEO_TS.IFO")))
144 return {};
145
146 auto titles_and_timestamps = parse_dvd(dvd_dir.u8string());
147
148 if (title > titles_and_timestamps.size())
149 throw parser_x{fmt::format(Y("The title number '{0}' is higher than the number of titles on the DVD ({1})."), title, titles_and_timestamps.size())};
150
151 return create_editions_and_chapters({ titles_and_timestamps[title - 1] }, language, {});
152 }
153
154 }
155
156 #endif // HAVE_DVDREAD
157