Skip to content

Commit a30f01e

Browse files
committed
fixup! plugins: mp4chapters_to_cue: Generate cue from Nero/Apple chapters in mp4 files
Change-Id: I7aef37687c82378d75912d59b146ec3ea01f5f2c
1 parent 89ee25a commit a30f01e

File tree

1 file changed

+74
-80
lines changed

1 file changed

+74
-80
lines changed

apps/plugins/mp4chapters_to_cue.c

Lines changed: 74 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ static void buffer_reset(void) {
8080
#define MP4_stsz 0x7374737A /* 'stsz' - Sample size atom */
8181
#define MP4_mdhd 0x6D646864 /* 'mdhd' - Media header atom */
8282

83+
#define MAX_LEN 256
8384
/* Structure to hold chapter information */
8485
struct chapter_info {
8586
uint64_t timestamp; /* Chapter start time in milliseconds */
86-
char title[256]; /* Chapter title */
87+
char title[MAX_LEN]; /* Chapter title */
8788
};
8889

8990
/* MP4 utility functions */
@@ -152,23 +153,24 @@ static int search_for_atom(int fd, off_t start_pos, off_t end_pos, uint32_t targ
152153
}
153154

154155
/* Parse Apple chapter track - look for text track with chapter data */
155-
static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size,
156-
struct chapter_info *chapters) {
156+
static struct chapter_info* parse_apple_chapter_track(int fd, off_t track_start, off_t track_size, int* num_chapters)
157+
{
158+
*num_chapters = 0;
157159
off_t track_end = track_start + track_size;
158160
int chapter_count = 0;
159161
uint32_t track_timescale = 1000; /* Default timescale */
160162

161163
/* Look for track header to check if this is a text track */
162164
off_t tkhd_pos, tkhd_size;
163165
if (!search_for_atom(fd, track_start, track_end, MP4_tkhd, &tkhd_pos, &tkhd_size)) {
164-
DEBUGF("No tkhd found in track");
166+
DEBUGF("No tkhd found in track\n");
165167
return 0;
166168
}
167169

168170
/* Look for media atom */
169171
off_t mdia_pos, mdia_size;
170172
if (!search_for_atom(fd, track_start, track_end, MP4_mdia, &mdia_pos, &mdia_size)) {
171-
DEBUGF("No mdia found in track");
173+
DEBUGF("No mdia found in track\n");
172174
return 0;
173175
}
174176

@@ -189,37 +191,37 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
189191
track_timescale = read_uint32be(fd);
190192
}
191193

192-
DEBUGF("Found track timescale: %u", track_timescale);
194+
DEBUGF("Found track timescale: %u\n", track_timescale);
193195
} else {
194-
DEBUGF("No mdhd found, using default timescale");
196+
DEBUGF("No mdhd found, using default timescale\n");
195197
}
196198

197199
/* Look for media handler to check track type */
198200
off_t hdlr_pos, hdlr_size;
199201
if (!search_for_atom(fd, mdia_pos, mdia_pos + mdia_size, MP4_hdlr, &hdlr_pos, &hdlr_size)) {
200-
DEBUGF("No hdlr found in track");
202+
DEBUGF("No hdlr found in track\n");
201203
return 0;
202204
}
203205

204206
/* Check if this is a text track */
205207
rb->lseek(fd, hdlr_pos + 8 + 8, SEEK_SET); /* Skip atom header + version/flags */
206208
uint32_t handler_type = read_uint32be(fd);
207209

208-
DEBUGF("Track handler type: %c%c%c%c",
210+
DEBUGF("Track handler type: %c%c%c%c\n",
209211
(char)(handler_type >> 24), (char)(handler_type >> 16),
210212
(char)(handler_type >> 8), (char)handler_type);
211213

212214
if (handler_type != MP4_TEXT && handler_type != MP4_tx3g) {
213-
DEBUGF("Not a text track, skipping");
215+
DEBUGF("Not a text track, skipping\n");
214216
return 0; /* Not a text track */
215217
}
216218

217-
DEBUGF("Found text track!");
219+
DEBUGF("Found text track!\n");
218220

219221
/* Look for media information atom first */
220222
off_t minf_pos, minf_size;
221223
if (!search_for_atom(fd, mdia_pos + 8, mdia_pos + mdia_size, MP4_minf, &minf_pos, &minf_size)) {
222-
DEBUGF("No minf found in text track");
224+
DEBUGF("No minf found in text track\n");
223225
return 0;
224226
}
225227

@@ -228,7 +230,7 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
228230
/* Look for sample table */
229231
off_t stbl_pos, stbl_size;
230232
if (!search_for_atom(fd, minf_pos + 8, minf_pos + minf_size, MP4_stbl, &stbl_pos, &stbl_size)) {
231-
DEBUGF("No stbl found in text track");
233+
DEBUGF("No stbl found in text track\n");
232234
return 0;
233235
}
234236

@@ -237,14 +239,14 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
237239
/* Get time-to-sample information */
238240
off_t stts_pos, stts_size;
239241
if (!search_for_atom(fd, stbl_pos + 8, stbl_pos + stbl_size, MP4_stts, &stts_pos, &stts_size)) {
240-
DEBUGF("No stts found");
242+
DEBUGF("No stts found\n");
241243
return 0;
242244
}
243245

244246
/* Get sample size information to know how many samples we have */
245247
off_t stsz_pos, stsz_size;
246248
if (!search_for_atom(fd, stbl_pos + 8, stbl_pos + stbl_size, MP4_stsz, &stsz_pos, &stsz_size)) {
247-
DEBUGF("No stsz found");
249+
DEBUGF("No stsz found\n");
248250
return 0;
249251
}
250252

@@ -271,12 +273,14 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
271273

272274
DEBUGF("Sample count: %u, default size: %u, timescale: %u\n", sample_count, default_sample_size, track_timescale);
273275

274-
if (sample_count == 0 || sample_count > 1000) {
276+
if (sample_count == 0) {
275277
return 0; /* Sanity check */
276278
}
277279

278280
// Allocate memory for chapters
279-
buffer_alloc(sizeof(struct chapter_info) * sample_count);
281+
struct chapter_info *chapters = buffer_alloc(sizeof(struct chapter_info) * sample_count);
282+
*num_chapters = sample_count;
283+
280284

281285
/* Calculate memory requirements */
282286
size_t sample_sizes_bytes = (default_sample_size == 0) ? sample_count * sizeof(uint32_t) : 0;
@@ -341,11 +345,16 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
341345
/* Build timestamp array from time-to-sample data */
342346
uint64_t current_time = 0;
343347
uint32_t sample_index = 0;
344-
uint32_t current_chunk = 0;
348+
uint64_t current_chunk_pos = 0; /* Position within current chunk */
345349

346350
/* Helper buffer for reading sample data */
347351
char sample_buffer[512];
348352

353+
/* Start at the beginning of the first chunk */
354+
if (chunk_count > 0) {
355+
current_chunk_pos = chunk_offsets[0];
356+
}
357+
349358
for (uint32_t i = 0; i < stts_entry_count; i++) {
350359
rb->lseek(fd, stts_pos + 8 + 8 + (i * 8), SEEK_SET);
351360
uint32_t samples_in_entry = read_uint32be(fd);
@@ -357,12 +366,12 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
357366
for (uint32_t j = 0; j < samples_in_entry && sample_index < sample_count; j++) {
358367
/* Convert from track timescale to milliseconds */
359368
chapters[chapter_count].timestamp = (current_time * 1000) / track_timescale;
360-
chapters[chapter_count].title[0] = 0;
369+
361370
DEBUGF("Chapter %d: time=%lu units, timestamp=%lu ms (timescale=%u)\n",
362371
chapter_count, (unsigned long)current_time, (unsigned long)chapters[chapter_count].timestamp, track_timescale);
363372

364373
/* Try to read the actual chapter title from sample data */
365-
if (chunk_offsets != NULL && current_chunk < chunk_count) {
374+
if (chunk_count > 0) {
366375
/* Get sample size */
367376
uint32_t sample_size;
368377
if (default_sample_size > 0) {
@@ -374,8 +383,8 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
374383
}
375384

376385
if (sample_size > 0 && sample_size < sizeof(sample_buffer)) {
377-
/* Read sample data */
378-
rb->lseek(fd, chunk_offsets[current_chunk], SEEK_SET);
386+
/* Read sample data from current position in chunk */
387+
rb->lseek(fd, current_chunk_pos, SEEK_SET);
379388
int bytes_read = rb->read(fd, sample_buffer, sample_size);
380389

381390
if (bytes_read > 0) {
@@ -388,12 +397,7 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
388397
uint16_t text_len = (sample_buffer[0] << 8) | sample_buffer[1];
389398
if (text_len > 0 && text_len < sample_size - 2 && text_len < 200) {
390399
title_text = &sample_buffer[2];
391-
/* Ensure null termination */
392-
size_t copy_len = text_len;
393-
if (copy_len >= sizeof(chapters[chapter_count].title)) {
394-
copy_len = sizeof(chapters[chapter_count].title) - 1;
395-
}
396-
rb->strlcpy(chapters[chapter_count].title, title_text, copy_len + 1);
400+
rb->strlcpy(chapters[chapter_count].title, title_text, MIN(text_len + 1, MAX_LEN));
397401
}
398402
}
399403

@@ -411,11 +415,8 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
411415
}
412416

413417
if (text_end - k > 3) { /* At least 4 characters */
414-
size_t copy_len = text_end - k;
415-
if (copy_len >= sizeof(chapters[chapter_count].title)) {
416-
copy_len = sizeof(chapters[chapter_count].title) - 1;
417-
}
418-
rb->strlcpy(chapters[chapter_count].title, &sample_buffer[k], copy_len + 1);
418+
size_t copy_len = text_end - k + 1;
419+
rb->strlcpy(chapters[chapter_count].title, &sample_buffer[k], MIN(copy_len, MAX_LEN));
419420
title_text = chapters[chapter_count].title;
420421
break;
421422
}
@@ -426,14 +427,14 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
426427
DEBUGF("Sample %u: size=%u, title='%s'\n", sample_index, sample_size,
427428
title_text ? chapters[chapter_count].title : "failed");
428429
}
430+
431+
/* Move to next sample position within the chunk */
432+
current_chunk_pos += sample_size;
429433
}
430-
431-
/* Move to next chunk for next sample (simplified - assumes one sample per chunk) */
432-
current_chunk++;
433434
}
434435

435436
/* Fallback to generic title if we couldn't read the text */
436-
if (chapters[chapter_count].title[0] == 0) {
437+
if (rb->strlen(chapters[chapter_count].title) == 0) {
437438
rb->snprintf(chapters[chapter_count].title, sizeof(chapters[chapter_count].title),
438439
"Chapter %d", chapter_count + 1);
439440
}
@@ -444,22 +445,21 @@ static int parse_apple_chapter_track(int fd, off_t track_start, off_t track_size
444445
}
445446
}
446447

447-
DEBUGF("Extracted %d chapters from text track", chapter_count);
448+
DEBUGF("Extracted %d chapters from text track\n", chapter_count);
448449

449-
return chapter_count;
450+
return chapters;
450451
}
451452

452453
/* Find and parse MP4 chapters (both Nero and Apple formats) */
453-
static int find_mp4_chapters(const char *mp4_path, struct chapter_info *chapters, int max_chapters) {
454+
static struct chapter_info* find_mp4_chapters(const char *mp4_path, int* chapter_count) {
454455
int fd = rb->open(mp4_path, O_RDONLY);
455-
if (fd < 0) return -1;
456+
if (fd < 0) return NULL;
456457

457-
int chapter_count = 0;
458458
uint32_t atom_size, atom_type;
459459
off_t file_pos = 0;
460460

461461
/* First pass: Look for Nero chpl atoms (simpler format) */
462-
while (chapter_count == 0) {
462+
while (1) {
463463
if (rb->lseek(fd, file_pos, SEEK_SET) < 0) break;
464464
if (rb->read(fd, &atom_size, 4) != 4) break;
465465
if (rb->read(fd, &atom_type, 4) != 4) break;
@@ -476,31 +476,32 @@ static int find_mp4_chapters(const char *mp4_path, struct chapter_info *chapters
476476
/* Found Nero chapter list atom */
477477
rb->lseek(fd, 8, SEEK_CUR); /* Skip version and flags */
478478
uint8_t num_chapters = read_uint8(fd);
479-
480-
for (int i = 0; i < num_chapters && chapter_count < max_chapters; i++) {
479+
struct chapter_info *chapters = buffer_alloc( sizeof(*chapters) * num_chapters);
480+
for (int i = 0; i < num_chapters; i++) {
481481
uint64_t timestamp_100ns = read_uint64be(fd);
482-
chapters[chapter_count].timestamp = timestamp_100ns / 10000; /* Convert from 100ns to milliseconds */
483-
484-
/* Read chapter title (Pascal string) */
485-
uint8_t title_len = read_uint8(fd);
486-
if (title_len > 0 && title_len < 255) {
487-
rb->read(fd, chapters[chapter_count].title, title_len);
488-
chapters[chapter_count].title[title_len] = '\0';
489-
} else {
490-
rb->snprintf(chapters[chapter_count].title, sizeof(chapters[chapter_count].title),
491-
"Chapter %d", chapter_count + 1);
492-
}
482+
chapters[i].timestamp = timestamp_100ns / 10000; /* Convert from 100ns to milliseconds */
493483

494-
chapter_count++;
484+
/* Read chapter title (Pascal string) */
485+
ssize_t title_len = read_uint8(fd);
486+
title_len = rb->read(fd, chapters[i].title, MIN(title_len, MAX_LEN -1));
487+
if (title_len > 0)
488+
chapters[i].title[title_len] = '\0';
489+
else
490+
rb->snprintf(chapters[i].title, sizeof(chapters[i].title),
491+
"Chapter %d", i + 1);
495492
}
496-
break;
493+
494+
*chapter_count = num_chapters;
495+
rb->close(fd);
496+
return chapters; /* Found Nero chapters */
497497
}
498498

499499
file_pos += atom_size;
500500
}
501501

502502
/* If no Nero chapters found, look for Apple chapters in moov atom */
503-
if (chapter_count == 0) {
503+
struct chapter_info *chapters = NULL;
504+
{
504505
file_pos = 0;
505506

506507
while (1) {
@@ -518,13 +519,13 @@ static int find_mp4_chapters(const char *mp4_path, struct chapter_info *chapters
518519

519520
if (atom_type == MP4_moov) {
520521
/* Found movie atom - look for chapter tracks */
521-
DEBUGF("Found moov atom, searching for tracks...");
522+
DEBUGF("Found moov atom, searching for tracks...\n");
522523
off_t moov_end = file_pos + atom_size;
523524
off_t moov_pos = file_pos + 8;
524525
int track_num = 0;
525526

526527
/* Look for tracks that might contain chapters */
527-
while (moov_pos < moov_end && chapter_count == 0) {
528+
while (moov_pos < moov_end && *chapter_count == 0) {
528529
rb->lseek(fd, moov_pos, SEEK_SET);
529530
uint32_t sub_size = read_uint32be(fd);
530531
uint32_t sub_type = read_uint32be(fd);
@@ -533,15 +534,15 @@ static int find_mp4_chapters(const char *mp4_path, struct chapter_info *chapters
533534

534535
if (sub_type == MP4_trak) {
535536
track_num++;
536-
DEBUGF("Checking track %d for chapters...", track_num);
537+
DEBUGF("Checking track %d for chapters...\n", track_num);
537538
/* Check if this track contains chapters */
538-
chapter_count = parse_apple_chapter_track(fd, moov_pos + 8, sub_size - 8,
539-
chapters);
540-
if (chapter_count > 0) {
541-
DEBUGF("Found %d chapters in track %d!", chapter_count, track_num);
539+
chapters = parse_apple_chapter_track(fd, moov_pos + 8, sub_size - 8, chapter_count);
540+
541+
if (*chapter_count > 0) {
542+
DEBUGF("Found %d chapters in track %d!\n", *chapter_count, track_num);
542543
break;
543544
} else {
544-
DEBUGF("Track %d: no chapters found", track_num);
545+
DEBUGF("Track %d: no chapters found\n", track_num);
545546
}
546547
}
547548

@@ -555,7 +556,7 @@ static int find_mp4_chapters(const char *mp4_path, struct chapter_info *chapters
555556
}
556557

557558
rb->close(fd);
558-
return chapter_count;
559+
return chapters; /* No chapters found */
559560
}
560561

561562
/* Generate CUE file content */
@@ -638,29 +639,22 @@ enum plugin_status plugin_start(const void* parameter) {
638639
/* Reset buffer before use */
639640
buffer_reset();
640641

641-
/* HACK. GET RAW BUFFER. DO ALLOCATION WHEN SIZE IS KNONW. Make sure no other allocations are done BEFORE*/
642-
struct chapter_info *chapters = (struct chapter_info*)buffer_alloc(0);
643-
if (!chapters) {
644-
rb->splash(HZ*2, "Out of memory for chapters");
645-
return PLUGIN_ERROR;
646-
}
647-
642+
648643
/* Find MP4 chapters */
649-
int chapter_count = find_mp4_chapters(mp4_path, chapters, 1000);
644+
int chapter_count;
645+
struct chapter_info *chapters = find_mp4_chapters(mp4_path, &chapter_count);
650646

651647
if (chapter_count <= 0) {
652648
rb->splash(HZ*2, "No chapters found");
653649
return PLUGIN_OK;
654650
}
655-
656-
rb->splashf(HZ, "Found %d chapters", chapter_count);
657-
651+
658652
/* Generate CUE file */
659653
if (generate_cue_file(mp4_path, chapters, chapter_count)) {
660654
rb->splashf(HZ*2, "CUE file created with %d tracks", chapter_count);
661655
return PLUGIN_OK;
662656
} else {
663-
rb->splash(HZ*2, "Failed to create CUE file");
657+
rb->splashf(HZ*2, "Failed to create CUE file with %d tracks", chapter_count);
664658
return PLUGIN_ERROR;
665659
}
666660
}

0 commit comments

Comments
 (0)