/* * Copyright (C) 2003-2005 M. Bakker, Nero AG, http://www.nero.com * FAAD2 - Freeware Advanced Audio (AAC) Decoder including SBR decoding * * See file COPYING. */ /** \file mp4.c Paraslash's internal mp4 parser. */ /* * This is a stripped down version of the former mp4ff library which used to be * part of the faad decoder project but was removed from the faad code base in * 2017. The original code has been cleaned up substantially and the public API * has been documented. See the git commit log for details. */ #include "para.h" #include "error.h" #include "portable_io.h" #include "string.h" #include "mp4.h" /** * The three states of the mp4 parser. The parser only loads the audio specific * values and tables when it is in the second state. */ enum audio_track_state { /** We haven't encountered an mp4a atom so far. */ ATS_INITIAL, /** We have seen an mp4a atom but no subsequent trak atom yet. */ ATS_SEEN_MP4A, /** A trak atom was seen *after* the mp4a atom. */ ATS_TRACK_CHANGE, }; struct mp4_track { /* determines which atoms we still need to parse. */ enum audio_track_state state; /* mp4a */ uint16_t channel_count; uint16_t sample_rate; /* stsz */ uint32_t stsz_sample_size; uint32_t stsz_sample_count; uint32_t *stsz_table; /* stts */ uint32_t stts_entry_count; uint32_t *stts_sample_count; /* stsc */ uint32_t stsc_entry_count; uint32_t *stsc_first_chunk; uint32_t *stsc_samples_per_chunk; /* stsc */ uint32_t stco_entry_count; uint32_t *stco_chunk_offset; /* mdhd */ uint32_t time_scale; uint64_t duration; }; struct mp4 { const struct mp4_callback *cb; uint64_t moov_offset; uint64_t moov_size; uint64_t meta_offset; uint32_t meta_size; uint64_t ilst_offset; uint32_t ilst_size; uint64_t udta_offset; uint32_t udta_size; uint8_t last_atom; struct mp4_track track; struct mp4_metadata meta; }; /* * Returns -E_MP4_READ, 0, or 1 on errors/EOF/success. Partial reads followed * by EOF or read errors are treated as errors. */ static int read_data(struct mp4 *f, void *data, size_t size) { while (size > 0) { ssize_t ret = f->cb->read(f->cb->user_data, data, size); if (ret < 0 && errno == EINTR) continue; /* regard EAGAIN as an error as reads should be blocking. */ if (ret <= 0) return ret < 0? -E_MP4_READ : 0; size -= ret; } return 1; } static int read_int64(struct mp4 *f, uint64_t *result) { uint8_t data[8]; int ret = read_data(f, data, 8); if (ret > 0) *result = read_u64_be(data); return ret; } static int read_int32(struct mp4 *f, uint32_t *result) { uint8_t data[4]; int ret = read_data(f, data, 4); if (ret > 0) *result = read_u32_be(data); return ret; } static int read_int16(struct mp4 *f, uint16_t *result) { uint8_t data[2]; int ret = read_data(f, data, 2); if (ret > 0) *result = read_u16_be(data); return ret; } /** \cond atom_items */ /* A macro defining the atoms we care about. It gets expanded twice. */ #define ATOM_ITEMS \ ATOM_ITEM(MOOV, 'm', 'o', 'o', 'v') /* movie (top-level container) */ \ ATOM_ITEM(TRAK, 't', 'r', 'a', 'k') /* container for a single track */ \ ATOM_ITEM(MDIA, 'm', 'd', 'i', 'a') /* media information */ \ ATOM_ITEM(MINF, 'm', 'i', 'n', 'f') /* extends mdia */ \ ATOM_ITEM(STBL, 's', 't', 'b', 'l') /* sample table container */ \ ATOM_ITEM(UDTA, 'u', 'd', 't', 'a') /* user data */ \ ATOM_ITEM(ILST, 'i', 'l', 's', 't') /* iTunes Metadata list */ \ ATOM_ITEM(ARTIST, 0xa9, 'A', 'R', 'T') /* artist */ \ ATOM_ITEM(TITLE, 0xa9, 'n', 'a', 'm') /* title */ \ ATOM_ITEM(ALBUM, 0xa9, 'a', 'l', 'b') /* album */ \ ATOM_ITEM(DATE, 0xa9, 'd', 'a', 'y') /* date */ \ ATOM_ITEM(COMMENT, 0xa9, 'c', 'm', 't') /* comment */ \ ATOM_ITEM(MDHD, 'm', 'd', 'h', 'd') /* track header */ \ ATOM_ITEM(STSD, 's', 't', 's', 'd') /* sample description box */ \ ATOM_ITEM(STTS, 's', 't', 't', 's') /* time to sample box */ \ ATOM_ITEM(STSZ, 's', 't', 's', 'z') /* sample size box */ \ ATOM_ITEM(STCO, 's', 't', 'c', 'o') /* chunk offset box */ \ ATOM_ITEM(STSC, 's', 't', 's', 'c') /* sample to chunk box */ \ ATOM_ITEM(MP4A, 'm', 'p', '4', 'a') /* mp4 audio */ \ ATOM_ITEM(META, 'm', 'e', 't', 'a') /* iTunes Metadata box */ \ ATOM_ITEM(DATA, 'd', 'a', 't', 'a') /* iTunes Metadata data box */ \ /** \endcond atom_items */ /** For the C enumeration we concatenate ATOM_ with the first argument. */ #define ATOM_ITEM(_name, a, b, c, d) ATOM_ ## _name, /** The enumeration of interesting atoms. */ enum atom {ATOM_ITEMS}; #undef ATOM_ITEM /** A cpp version of read_u32_be(). */ #define ATOM_VALUE(a, b, c, d) ((a << 24) + (b << 16) + (c << 8) + d) static uint8_t atom_name_to_type(uint8_t *p) { /** Expands to an instance of the following unnamed structure. */ #define ATOM_ITEM(_name, a, b, c, d) \ {.name = # _name, .val = ATOM_VALUE(a, b, c, d)}, static const struct { const char *name; uint32_t val; } atom_table[] = {ATOM_ITEMS}; #undef ATOM_ITEM uint32_t val = read_u32_be(p); for (uint8_t n = 0; n < ARRAY_SIZE(atom_table); n++) if (val == atom_table[n].val) return n; return 255; } /* read atom header, atom size is returned with header included. */ static int atom_read_header(struct mp4 *f, uint8_t *atom_type, uint8_t *header_size, uint64_t *atom_size) { uint32_t size; int ret; uint8_t atom_header[8]; ret = read_data(f, atom_header, 8); if (ret <= 0) return ret; size = read_u32_be(atom_header); if (size == 1) { /* 64 bit atom size */ if (header_size) *header_size = 16; ret = read_int64(f, atom_size); if (ret <= 0) return ret; } else { if (header_size) *header_size = 8; *atom_size = size; } *atom_type = atom_name_to_type(atom_header + 4); return 1; } static off_t get_position(const struct mp4 *f) { return f->cb->seek(f->cb->user_data, 0, SEEK_CUR); } static void set_position(struct mp4 *f, off_t position) { f->cb->seek(f->cb->user_data, position, SEEK_SET); } static void skip_bytes(struct mp4 *f, off_t num_skip) { f->cb->seek(f->cb->user_data, num_skip, SEEK_CUR); } static int read_stsz(struct mp4 *f) { int ret; struct mp4_track *t = &f->track; if (t->state != ATS_SEEN_MP4A || t->stsz_table) return 1; skip_bytes(f, 4); /* version (1), flags (3) */ ret = read_int32(f, &t->stsz_sample_size); if (ret <= 0) return ret; ret = read_int32(f, &t->stsz_sample_count); if (ret <= 0) return ret; if (t->stsz_sample_size != 0) return 1; t->stsz_table = arr_alloc(t->stsz_sample_count, sizeof(int32_t)); for (uint32_t n = 0; n < t->stsz_sample_count; n++) { ret = read_int32(f, &t->stsz_table[n]); if (ret <= 0) return ret; } return 1; } static int read_stts(struct mp4 *f) { int ret; struct mp4_track *t = &f->track; if (t->state != ATS_SEEN_MP4A || t->stts_sample_count) return 1; skip_bytes(f, 4); /* version (1), flags (3) */ ret = read_int32(f, &t->stts_entry_count); if (ret <= 0) return ret; t->stts_sample_count = arr_alloc(t->stts_entry_count, sizeof(int32_t)); for (uint32_t n = 0; n < t->stts_entry_count; n++) { ret = read_int32(f, &t->stts_sample_count[n]); if (ret <= 0) return ret; skip_bytes(f, 4); /* sample delta */ } return 1; } static int read_stsc(struct mp4 *f) { int ret; struct mp4_track *t = &f->track; if (t->state != ATS_SEEN_MP4A) return 1; if (t->stsc_first_chunk || t->stsc_samples_per_chunk) return 1; skip_bytes(f, 4); /* version (1), flags (3) */ ret = read_int32(f, &t->stsc_entry_count); if (ret <= 0) return ret; t->stsc_first_chunk = arr_alloc(t->stsc_entry_count, sizeof(int32_t)); t->stsc_samples_per_chunk = arr_alloc(t->stsc_entry_count, sizeof (int32_t)); for (uint32_t n = 0; n < t->stsc_entry_count; n++) { ret = read_int32(f, &t->stsc_first_chunk[n]); if (ret <= 0) return ret; ret = read_int32(f, &t->stsc_samples_per_chunk[n]); if (ret <= 0) return ret; skip_bytes(f, 4); /* sample desc index */ } return 1; } static int read_stco(struct mp4 *f) { int ret; struct mp4_track *t = &f->track; if (t->state != ATS_SEEN_MP4A || t->stco_chunk_offset) return 1; skip_bytes(f, 4); /* version (1), flags (3) */ ret = read_int32(f, &t->stco_entry_count); if (ret <= 0) return ret; t->stco_chunk_offset = arr_alloc(t->stco_entry_count, sizeof(int32_t)); for (uint32_t n = 0; n < t->stco_entry_count; n++) { ret = read_int32(f, &t->stco_chunk_offset[n]); if (ret <= 0) return ret; } return 1; } static int read_stsd(struct mp4 *f) { int ret; uint32_t entry_count; if (f->track.state != ATS_INITIAL) return 1; skip_bytes(f, 4); /* version (1), flags (3) */ ret = read_int32(f, &entry_count); if (ret <= 0) return ret; for (uint32_t n = 0; n < entry_count; n++) { uint64_t skip = get_position(f); uint64_t size; uint8_t atom_type = 0; ret = atom_read_header(f, &atom_type, NULL, &size); if (ret <= 0) return ret; skip += size; if (atom_type == ATOM_MP4A) { f->track.state = ATS_SEEN_MP4A; /* reserved (6), data reference index (2), reserved (8) */ skip_bytes(f, 16); ret = read_int16(f, &f->track.channel_count); if (ret <= 0) return ret; skip_bytes(f, 6); ret = read_int16(f, &f->track.sample_rate); if (ret <= 0) return ret; } set_position(f, skip); } return 1; } static const char *get_metadata_name(uint8_t atom_type) { switch (atom_type) { case ATOM_TITLE: return "title"; case ATOM_ARTIST: return "artist"; case ATOM_ALBUM: return "album"; case ATOM_DATE: return "date"; case ATOM_COMMENT: return "comment"; default: return "unknown"; } } static int parse_tag(struct mp4 *f, uint8_t parent, int32_t size) { int ret; uint64_t subsize, sumsize; char *value = NULL; uint32_t len = 0; uint64_t destpos; struct mp4_tag *tag; for ( sumsize = 0; sumsize < size; set_position(f, destpos), sumsize += subsize ) { uint8_t atom_type; uint8_t header_size = 0; ret = atom_read_header(f, &atom_type, &header_size, &subsize); if (ret <= 0) goto fail; destpos = get_position(f) + subsize - header_size; if (atom_type != ATOM_DATA) continue; skip_bytes(f, 8); /* version (1), flags (3), reserved (4) */ ret = -E_MP4_CORRUPT; if (subsize < header_size + 8 || subsize > UINT_MAX) goto fail; len = subsize - (header_size + 8); free(value); value = alloc(len + 1); ret = read_data(f, value, len); if (ret <= 0) goto fail; value[len] = '\0'; } if (!value) return -E_MP4_CORRUPT; f->meta.tags = para_realloc(f->meta.tags, (f->meta.count + 1) * sizeof(struct mp4_tag)); tag = f->meta.tags + f->meta.count; tag->item = para_strdup(get_metadata_name(parent)); tag->value = value; f->meta.count++; return 1; fail: free(value); return ret; } static int read_mdhd(struct mp4 *f) { int ret; uint32_t version; struct mp4_track *t = &f->track; if (t->state != ATS_INITIAL) return 1; ret = read_int32(f, &version); if (ret <= 0) return ret; if (version == 1) { skip_bytes(f, 16); /* creation time (8), modification time (8) */ ret = read_int32(f, &t->time_scale); if (ret <= 0) return ret; ret = read_int64(f, &t->duration); if (ret <= 0) return ret; } else { /* version == 0 */ uint32_t temp; skip_bytes(f, 8); /* creation time (4), modification time (4) */ ret = read_int32(f, &t->time_scale); if (ret <= 0) return ret; ret = read_int32(f, &temp); if (ret <= 0) return ret; t->duration = (temp == (uint32_t) (-1))? (uint64_t) (-1) : (uint64_t) (temp); } skip_bytes(f, 4); return 1; } static int read_ilst(struct mp4 *f, int32_t size) { int ret; uint64_t sumsize = 0; while (sumsize < size) { uint8_t atom_type; uint64_t subsize, destpos; uint8_t header_size = 0; ret = atom_read_header(f, &atom_type, &header_size, &subsize); if (ret <= 0) return ret; destpos = get_position(f) + subsize - header_size; switch (atom_type) { case ATOM_ARTIST: case ATOM_TITLE: case ATOM_ALBUM: case ATOM_COMMENT: case ATOM_DATE: ret = parse_tag(f, atom_type, subsize - header_size); if (ret <= 0) return ret; } set_position(f, destpos); sumsize += subsize; } return 1; } static int read_meta(struct mp4 *f, uint64_t size) { int ret; uint64_t subsize, sumsize = 0; uint8_t atom_type; uint8_t header_size = 0; skip_bytes(f, 4); /* version (1), flags (3) */ while (sumsize < (size - (header_size + 4))) { ret = atom_read_header(f, &atom_type, &header_size, &subsize); if (ret <= 0) return ret; if (subsize <= header_size + 4) return 1; if (atom_type == ATOM_ILST) { f->ilst_offset = get_position(f) - header_size; f->ilst_size = subsize; ret = read_ilst(f, subsize - (header_size + 4)); if (ret <= 0) return ret; } else set_position(f, get_position(f) + subsize - header_size); sumsize += subsize; } return 1; } static bool need_atom(uint8_t atom_type, bool meta_only) { /* these are needed in any case */ switch (atom_type) { case ATOM_STSD: case ATOM_META: case ATOM_TRAK: case ATOM_MDIA: case ATOM_MINF: case ATOM_STBL: case ATOM_UDTA: return true; } /* meta-only opens don't need anything else */ if (meta_only) return false; /* these are only required for regular opens */ switch (atom_type) { case ATOM_STTS: case ATOM_STSZ: case ATOM_STCO: case ATOM_STSC: case ATOM_MDHD: return true; } return false; } /* parse atoms that are sub atoms of other atoms */ static int parse_sub_atoms(struct mp4 *f, uint64_t total_size, bool meta_only) { int ret; uint64_t dest, size, end = get_position(f) + total_size; for (dest = get_position(f); dest < end; set_position(f, dest)) { uint8_t header_size, atom_type; ret = atom_read_header(f, &atom_type, &header_size, &size); if (ret <= 0) return ret; if (size == 0) return -E_MP4_CORRUPT; dest = get_position(f) + size - header_size; if (atom_type == ATOM_TRAK && f->track.state == ATS_SEEN_MP4A) { f->track.state = ATS_TRACK_CHANGE; continue; } if (atom_type == ATOM_UDTA) { f->udta_offset = get_position(f) - header_size; f->udta_size = size; } if (!need_atom(atom_type, meta_only)) continue; switch (atom_type) { case ATOM_STSZ: ret = read_stsz(f); break; case ATOM_STTS: ret = read_stts(f); break; case ATOM_STSC: ret = read_stsc(f); break; case ATOM_STCO: ret = read_stco(f); break; case ATOM_STSD: ret = read_stsd(f); break; case ATOM_MDHD: ret = read_mdhd(f); break; case ATOM_META: f->meta_offset = get_position(f) - header_size; f->meta_size = size; ret = read_meta(f, size); break; default: ret = parse_sub_atoms(f, size - header_size, meta_only); } if (ret <= 0) return ret; } return 1; } /** * Deallocate all resources associated with an mp4 file handle. * * \param f File handle returned by \ref mp4_open() or \ref mp4_open_meta(). * * This frees the metadata items and various tables which were allocated when * the file was opened. The given file handle must not be NULL. */ void mp4_close(struct mp4 *f) { free(f->track.stsz_table); free(f->track.stts_sample_count); free(f->track.stsc_first_chunk); free(f->track.stsc_samples_per_chunk); free(f->track.stco_chunk_offset); for (uint32_t n = 0; n < f->meta.count; n++) { free(f->meta.tags[n].item); free(f->meta.tags[n].value); } free(f->meta.tags); free(f); } static int open_file(const struct mp4_callback *cb, bool meta_only, struct mp4 **result) { int ret; uint64_t size; uint8_t atom_type, header_size; struct mp4 *f = zalloc(sizeof(*f)); f->cb = cb; while ((ret = atom_read_header(f, &atom_type, &header_size, &size)) > 0) { f->last_atom = atom_type; if (atom_type != ATOM_MOOV || size <= header_size) { /* skip */ set_position(f, get_position(f) + size - header_size); continue; } f->moov_offset = get_position(f) - header_size; f->moov_size = size; ret = parse_sub_atoms(f, size - header_size, meta_only); if (ret <= 0) break; } if (ret < 0) goto fail; ret = -E_MP4_TRACK; if (f->track.channel_count == 0) goto fail; ret = -E_MP4_BAD_SAMPLERATE; if (f->track.sample_rate == 0) goto fail; ret = -E_MP4_MISSING_ATOM; if (f->udta_size == 0 || f->meta_size == 0 || f->ilst_size == 0) goto fail; *result = f; return 1; fail: *result = NULL; mp4_close(f); return ret; } /** * Read the audio track and the metadata of an mp4 file. * * \param cb Only the ->read() and ->seek() methods need to be supplied. * \param result Initialized to a non-NULL pointer iff the function succeeds. * * This detects and parses the first audio track and the metadata information * of the mp4 file. Various error checks are performed after the mp4 atoms have * been parsed successfully. * * This function does not modify the file. However, if the caller intents to * update the metadata later, the ->write() and ->truncate() methods must be * supplied in the callback structure. * * \return Standard. Several errors are possible. * * \sa \ref mp4_open_meta(). */ int mp4_open(const struct mp4_callback *cb, struct mp4 **result) { struct mp4 *f; int ret; *result = NULL; ret = open_file(cb, false, &f); if (ret < 0) return ret; ret = -E_MP4_BAD_SAMPLE_COUNT; if (f->track.stsz_sample_count == 0) goto fail; ret = -E_MP4_CORRUPT; if (f->track.time_scale == 0) goto fail; *result = f; return 1; fail: mp4_close(f); return ret; } static int32_t chunk_of_sample(const struct mp4 *f, int32_t sample, int32_t *chunk) { const struct mp4_track *t = &f->track; uint32_t *fc = t->stsc_first_chunk, *spc = t->stsc_samples_per_chunk; uint32_t chunk1, chunk1samples, n, total, k; for (k = 1, total = 0; k < t->stsc_entry_count; k++, total += n) { n = (fc[k] - fc[k - 1]) * spc[k - 1]; /* number of samples */ if (sample < total + n) break; } chunk1 = fc[k - 1]; chunk1samples = spc[k - 1]; if (chunk1samples != 0) *chunk = (sample - total) / chunk1samples + chunk1; else *chunk = 1; return total + (*chunk - chunk1) * chunk1samples; } /** * Compute the duration of an mp4 file. * * \param f See \ref mp4_close(). * * \return The number of milliseconds of the audio track. This function never * fails. */ uint64_t mp4_get_duration(const struct mp4 *f) { const struct mp4_track *t = &f->track; return t->duration * 1000 / t->time_scale; } /** * Reposition the read/write file offset. * * \param f See \ref mp4_close(). * \param sample The number of the sample to reposition to. * * The given sample number must be within range, i.e., strictly less than the * value returned by \ref mp4_num_samples(). * * \return Standard. The only possible error is an invalid sample number. */ int mp4_set_sample_position(struct mp4 *f, uint32_t sample) { const struct mp4_track *t = &f->track; int32_t offset, chunk, chunk_sample; uint32_t n, srs; /* sample range size */ if (sample >= t->stsz_sample_count) return -ERRNO_TO_PARA_ERROR(EINVAL); chunk_sample = chunk_of_sample(f, sample, &chunk); if (t->stsz_sample_size > 0) srs = (sample - chunk_sample) * t->stsz_sample_size; else { for (srs = 0, n = chunk_sample; n < sample; n++) srs += t->stsz_table[n]; } if (t->stco_entry_count > 0 && chunk > t->stco_entry_count) offset = t->stco_chunk_offset[t->stco_entry_count - 1]; else if (t->stco_entry_count > 0) offset = t->stco_chunk_offset[chunk - 1]; else offset = 8; set_position(f, offset + srs); return 1; } /** * Look up and return the size of the given sample in the stsz table. * * \param f See \ref mp4_close(). * \param sample The sample number of interest. * \param result Sample size is returned here. * * For the sample argument the restriction mentioned in the documentation of * \ref mp4_set_sample_position() applies as well. * * \return Standard. Like for \ref mp4_set_sample_position(), EINVAL is the * only possible error. */ int mp4_get_sample_size(const struct mp4 *f, uint32_t sample, uint32_t *result) { const struct mp4_track *t = &f->track; if (sample >= t->stsz_sample_count) return -ERRNO_TO_PARA_ERROR(EINVAL); if (t->stsz_sample_size != 0) *result = t->stsz_sample_size; else *result = t->stsz_table[sample]; return 1; } /** * Return the sample rate stored in the stsd atom. * * \param f See \ref mp4_close(). * * The sample rate is a property of the audio track of the mp4 file and is thus * independent of the sample number. * * \return The function always returns a positive value because the open * operation fails if the sample rate happens to be zero. A typical value is * 44100. */ uint16_t mp4_get_sample_rate(const struct mp4 *f) { return f->track.sample_rate; } /** * Return the number of channels of the audio track. * * \param f See \ref mp4_close(). * * \return The returned channel count is guaranteed to be positive because the * open operation fails if the mp4a atom is missing or contains a zero channel * count. */ uint16_t mp4_get_channel_count(const struct mp4 *f) { return f->track.channel_count; } /** * Return the number of samples of the audio track. * * \param f See \ref mp4_close(). * * \return The sample count is read from the stsz atom during open. */ uint32_t mp4_num_samples(const struct mp4 *f) { return f->track.stsz_sample_count; } /** * Open an mp4 file in metadata-only mode. * * \param cb See \ref mp4_open(). * \param result See \ref mp4_open(). * * This is similar to \ref mp4_open() but is cheaper because it only parses the * metadata of the mp4 file. The only functions that can subsequently be called * with the file handle returned here are \ref mp4_get_meta() and \ref * mp4_update_meta(). * * \return Standard. * * \sa \ref mp4_open(). The comment about ->write() and ->truncate() applies to * this function as well. */ int mp4_open_meta(const struct mp4_callback *cb, struct mp4 **result) { struct mp4 *f; int ret = open_file(cb, true, &f); if (ret < 0) return ret; *result = f; return 1; } /** * Return the metadata of an mp4 file. * * \param f See \ref mp4_close(). * * The caller is allowed to add, delete or modify the entries of the returned * structure with the intention to pass the modified version to \ref * mp4_update_meta(). * * \return This never returns NULL, even if the file contains no metadata tag * items. However, the meta count will be zero and the ->tags pointer NULL in * this case. */ struct mp4_metadata *mp4_get_meta(struct mp4 *f) { return &f->meta; } /** Total length of an on-disk metadata tag. */ #define TAG_LEN(_len) (24 + (_len)) static void create_ilst(const struct mp4_metadata *meta, uint8_t *out) { for (unsigned n = 0; n < meta->count; n++) { struct mp4_tag *tag = meta->tags + n; unsigned len = strlen(tag->value); const char *atom_name; if (!strcasecmp(tag->item, "title")) atom_name = "\xA9" "nam"; else if (!strcasecmp(tag->item, "artist")) atom_name = "\xA9" "ART"; else if (!strcasecmp(tag->item, "album")) atom_name = "\xA9" "alb"; else if (!strcasecmp(tag->item, "date")) atom_name = "\xA9" "day"; else if (!strcasecmp(tag->item, "comment")) atom_name = "\xA9" "cmt"; else assert(false); write_u32_be(out, TAG_LEN(len)); memcpy(out + 4, atom_name, 4); write_u32_be(out + 8, 8 /* data atom header */ + 8 /* flags + reserved */ + len); memcpy(out + 12, "data", 4); write_u32_be(out + 16, 1); /* flags */ write_u32_be(out + 20, 0); /* reserved */ memcpy(out + 24, tag->value, len); out += TAG_LEN(len); } } static void *modify_moov(struct mp4 *f, uint32_t *out_size) { int ret; uint64_t total_base = f->moov_offset + 8; uint32_t total_size = f->moov_size - 8; uint32_t new_ilst_size = 0; void *out_buffer; uint8_t *p_out; int32_t size_delta; uint32_t tmp; for (unsigned n = 0; n < f->meta.count; n++) new_ilst_size += TAG_LEN(strlen(f->meta.tags[n].value)); size_delta = new_ilst_size - (f->ilst_size - 8); *out_size = total_size + size_delta; out_buffer = alloc(*out_size); p_out = out_buffer; set_position(f, total_base); ret = read_data(f, p_out, f->udta_offset - total_base); if (ret <= 0) return NULL; p_out += f->udta_offset - total_base; ret = read_int32(f, &tmp); if (ret <= 0) return NULL; write_u32_be(p_out, tmp + size_delta); p_out += 4; ret = read_data(f, p_out, 4); if (ret <= 0) return NULL; p_out += 4; ret = read_data(f, p_out, f->meta_offset - f->udta_offset - 8); if (ret <= 0) return NULL; p_out += f->meta_offset - f->udta_offset - 8; ret = read_int32(f, &tmp); if (ret <= 0) return NULL; write_u32_be(p_out, tmp + size_delta); p_out += 4; ret = read_data(f, p_out, 4); if (ret <= 0) return NULL; p_out += 4; ret = read_data(f, p_out, f->ilst_offset - f->meta_offset - 8); if (ret <= 0) return NULL; p_out += f->ilst_offset - f->meta_offset - 8; ret = read_int32(f, &tmp); if (ret <= 0) return NULL; write_u32_be(p_out, tmp + size_delta); p_out += 4; ret = read_data(f, p_out, 4); if (ret <= 0) return NULL; p_out += 4; create_ilst(&f->meta, p_out); p_out += new_ilst_size; set_position(f, f->ilst_offset + f->ilst_size); ret = read_data(f, p_out, total_size - (f->ilst_offset - total_base) - f->ilst_size); if (ret <= 0) return NULL; return out_buffer; } static int write_data(struct mp4 *f, void *data, size_t size) { while (size > 0) { ssize_t ret = f->cb->write(f->cb->user_data, data, size); if (ret < 0) { if (errno == EINTR) continue; return -ERRNO_TO_PARA_ERROR(errno); } size -= ret; } return 1; } /** * Write back the modified metadata items to the mp4 file. * * This is the only public function which modifies the contents of an mp4 file. * This is achieved by calling the ->write() and ->truncate() methods of the * callback structure passed to \ref mp4_open() or \ref mp4_open_meta(). * * \param f See \ref mp4_close(). * * The modified metadata structure does not need to be supplied to this * function because it is part of the mp4 structure. * * \return Standard. */ int mp4_update_meta(struct mp4 *f) { void *new_moov_data; uint32_t new_moov_size; __nonstring uint8_t buf[8] = "----moov"; int ret; set_position(f, 0); new_moov_data = modify_moov(f, &new_moov_size); if (!new_moov_data ) { mp4_close(f); return 0; } if (f->last_atom != ATOM_MOOV) { set_position(f, f->moov_offset + 4); ret = write_data(f, "free", 4); /* rename old moov to free */ if (ret < 0) goto free_moov; /* write new moov atom at EOF */ f->cb->seek(f->cb->user_data, 0, SEEK_END); } else /* overwrite old moov atom */ set_position(f, f->moov_offset); write_u32_be(buf, new_moov_size + 8); ret = write_data(f, buf, sizeof(buf)); if (ret < 0) goto free_moov; ret = write_data(f, new_moov_data, new_moov_size); if (ret < 0) goto free_moov; ret = f->cb->truncate(f->cb->user_data); if (ret < 0) ret = -ERRNO_TO_PARA_ERROR(errno); free_moov: free(new_moov_data); return ret; } /** * Return the value of the given tag item. * * \param f See \ref mp4_close(). * \param item "artist", "title", "album", "comment", or "date". * * \return The function returns NULL if the given item is not in the above * list. Otherwise, if the file does not contain a tag for the given item, the * function also returns NULL. Otherwise a copy of the tag value is returned * and the caller should free this memory when it is no longer needed. */ __malloc char *mp4_get_tag_value(const struct mp4 *f, const char *item) { for (unsigned n = 0; n < f->meta.count; n++) if (!strcasecmp(f->meta.tags[n].item, item)) return para_strdup(f->meta.tags[n].value); return NULL; }