#include <neaacdec.h>
#include "para.h"
-
-/* To get the mp4ff_tag_t and mp4ff_metadata_t typedefs. */
-#define USE_TAGGING
-#include <mp4ff.h>
-
+#include "mp4.h"
#include "error.h"
#include "portable_io.h"
#include "afh.h"
const void *map;
size_t mapsize;
size_t fpos;
- int32_t track;
- mp4ff_t *mp4ff;
- mp4AudioSpecificConfig masc;
- mp4ff_callback_t cb;
+ struct mp4 *mp4;
+ struct mp4_callback cb;
};
-static uint32_t aac_afh_read_cb(void *user_data, void *dest, uint32_t want)
+static ssize_t aac_afh_read_cb(void *user_data, void *dest, size_t want)
{
struct aac_afh_context *c = user_data;
size_t have, rv;
if (want == 0 || c->fpos >= c->mapsize)
return 0;
have = c->mapsize - c->fpos;
- rv = PARA_MIN(have, (size_t)want);
+ rv = PARA_MIN(have, want);
PARA_DEBUG_LOG("reading %zu bytes @%zu\n", rv, c->fpos);
memcpy(dest, c->map + c->fpos, rv);
c->fpos += rv;
return rv;
}
-static uint32_t aac_afh_seek_cb(void *user_data, uint64_t pos)
+static off_t aac_afh_seek_cb(void *user_data, off_t offset, int whence)
{
struct aac_afh_context *c = user_data;
- c->fpos = pos;
- return 0;
-}
-static int32_t aac_afh_get_track(mp4ff_t *mp4ff, mp4AudioSpecificConfig *masc)
-{
- int32_t i, rc, num_tracks = mp4ff_total_tracks(mp4ff);
-
- assert(num_tracks >= 0);
- for (i = 0; i < num_tracks; i++) {
- unsigned char *buf = NULL;
- unsigned buf_size = 0;
-
- mp4ff_get_decoder_config(mp4ff, i, &buf, &buf_size);
- if (buf) {
- rc = NeAACDecAudioSpecificConfig(buf, buf_size, masc);
- free(buf);
- if (rc < 0)
- continue;
- return i;
- }
- }
- return -1; /* no audio track */
+ if (whence == SEEK_SET)
+ c->fpos = offset;
+ else if (whence == SEEK_CUR)
+ c->fpos += offset;
+ else if (whence == SEEK_END)
+ c->fpos = c->mapsize + offset;
+ else
+ assert(false);
+ return c->fpos;
}
static int aac_afh_open(const void *map, size_t mapsize, void **afh_context)
c->cb.seek = aac_afh_seek_cb;
c->cb.user_data = c;
- ret = -E_MP4FF_OPEN;
- c->mp4ff = mp4ff_open_read(&c->cb);
- if (!c->mp4ff)
+ ret = mp4_open(&c->cb, &c->mp4);
+ if (ret < 0)
goto free_ctx;
- c->track = aac_afh_get_track(c->mp4ff, &c->masc);
- ret = -E_MP4FF_TRACK;
- if (c->track < 0)
- goto close_mp4ff;
*afh_context = c;
return 0;
-close_mp4ff:
- mp4ff_close(c->mp4ff);
free_ctx:
free(c);
*afh_context = NULL;
static void aac_afh_close(void *afh_context)
{
struct aac_afh_context *c = afh_context;
- mp4ff_close(c->mp4ff);
+ mp4_close(c->mp4);
free(c);
}
-/**
- * Libmp4ff function to reposition the file to the given sample.
- *
- * \param f The opaque handle returned by mp4ff_open_read().
- * \param track The number of the (audio) track.
- * \param sample Destination.
- *
- * We need this function to obtain the offset of the sample within the audio
- * file. Unfortunately, it is not exposed in the mp4ff header.
- *
- * \return This function always returns 0.
- */
-int32_t mp4ff_set_sample_position(mp4ff_t *f, const int32_t track, const int32_t sample);
-
static int aac_afh_get_chunk(uint32_t chunk_num, void *afh_context,
const char **buf, uint32_t *len)
{
struct aac_afh_context *c = afh_context;
- int32_t ss;
+ uint32_t ss;
size_t offset;
+ int ret;
- assert(chunk_num <= INT_MAX);
- /* this function always returns zero */
- mp4ff_set_sample_position(c->mp4ff, c->track, chunk_num);
+ ret = mp4_set_sample_position(c->mp4, chunk_num);
+ if (ret < 0)
+ return ret;
offset = c->fpos;
- ss = mp4ff_read_sample_getsize(c->mp4ff, c->track, chunk_num);
- if (ss <= 0)
- return -E_MP4FF_BAD_SAMPLE;
- assert(ss + offset <= c->mapsize);
+ ret = mp4_get_sample_size(c->mp4, chunk_num, &ss);
+ if (ret < 0)
+ return ret;
+ if (ss + offset > c->mapsize) /* file got truncated?! */
+ return -E_MP4_CORRUPT;
*buf = c->map + offset;
*len = ss;
return 1;
}
-static void _aac_afh_get_taginfo(const mp4ff_t *mp4ff, struct taginfo *tags)
+static void aac_afh_get_taginfo(const struct mp4 *mp4, struct taginfo *tags)
{
- mp4ff_meta_get_artist(mp4ff, &tags->artist);
- mp4ff_meta_get_title(mp4ff, &tags->title);
- mp4ff_meta_get_date(mp4ff, &tags->year);
- mp4ff_meta_get_album(mp4ff, &tags->album);
- mp4ff_meta_get_comment(mp4ff, &tags->comment);
+ tags->artist = mp4_get_tag_value(mp4, "artist");
+ tags->title = mp4_get_tag_value(mp4, "title");
+ tags->year = mp4_get_tag_value(mp4, "date");
+ tags->album = mp4_get_tag_value(mp4, "album");
+ tags->comment = mp4_get_tag_value(mp4, "comment");
}
/*
struct afh_info *afhi)
{
int ret;
- int32_t rv;
struct aac_afh_context *c;
- int64_t tmp;
+ uint64_t milliseconds;
const char *buf;
uint32_t n, len;
if (ret < 0)
return ret;
- ret = -E_MP4FF_BAD_SAMPLERATE;
- rv = mp4ff_get_sample_rate(c->mp4ff, c->track);
- if (rv <= 0)
- goto close;
- afhi->frequency = rv;
-
- ret = -E_MP4FF_BAD_CHANNEL_COUNT;
- rv = mp4ff_get_channel_count(c->mp4ff, c->track);
- if (rv <= 0)
- goto close;
- afhi->channels = rv;
-
- ret = -E_MP4FF_BAD_SAMPLE_COUNT;
- rv = mp4ff_num_samples(c->mp4ff, c->track);
- if (rv <= 0)
- goto close;
- afhi->chunks_total = rv;
+ afhi->frequency = mp4_get_sample_rate(c->mp4);
+ assert(afhi->frequency > 0);
+ afhi->channels = mp4_get_channel_count(c->mp4);
+ assert(afhi->channels > 0);
+ afhi->chunks_total = mp4_num_samples(c->mp4);
+ assert(afhi->chunks_total > 0);
+
afhi->max_chunk_size = 0;
for (n = 0; n < afhi->chunks_total; n++) {
- if (aac_afh_get_chunk(n, c, &buf, &len) < 0)
- break;
+ ret = aac_afh_get_chunk(n, c, &buf, &len);
+ if (ret < 0)
+ goto out;
afhi->max_chunk_size = PARA_MAX(afhi->max_chunk_size, len);
}
-
- tmp = c->masc.sbr_present_flag == 1? 2048 : 1024;
- afhi->seconds_total = tmp * afhi->chunks_total / afhi->frequency;
- ms2tv(1000 * tmp / afhi->frequency, &afhi->chunk_tv);
-
- if (aac_afh_get_chunk(0, c, &buf, &len) >= 0)
- numbytes -= buf - map;
+ milliseconds = mp4_get_duration(c->mp4);
+ afhi->seconds_total = milliseconds / 1000;
+ ms2tv(milliseconds / afhi->chunks_total, &afhi->chunk_tv);
+ if (aac_afh_get_chunk(0, c, &buf, &len) < 0)
+ goto out;
+ numbytes -= buf - map;
afhi->bitrate = 8 * numbytes / afhi->seconds_total / 1000;
- _aac_afh_get_taginfo(c->mp4ff, &afhi->tags);
+ aac_afh_get_taginfo(c->mp4, &afhi->tags);
ret = 1;
-close:
+out:
aac_afh_close(c);
return ret;
}
-static uint32_t aac_afh_meta_read_cb(void *user_data, void *dest, uint32_t want)
+static ssize_t aac_afh_meta_read_cb(void *user_data, void *dest, size_t want)
{
int fd = *(int *)user_data;
return read(fd, dest, want);
}
-static uint32_t aac_afh_meta_seek_cb(void *user_data, uint64_t pos)
+static off_t aac_afh_meta_seek_cb(void *user_data, off_t offset, int whence)
{
int fd = *(int *)user_data;
- return lseek(fd, pos, SEEK_SET);
+ off_t ret = lseek(fd, offset, whence);
+
+ assert(ret != (off_t)-1);
+ return ret;
}
-static uint32_t aac_afh_meta_write_cb(void *user_data, void *dest, uint32_t want)
+static ssize_t aac_afh_meta_write_cb(void *user_data, void *dest, size_t count)
{
int fd = *(int *)user_data;
- return write(fd, dest, want);
+ return write(fd, dest, count);
}
-static uint32_t aac_afh_meta_truncate_cb(void *user_data)
+static int aac_afh_meta_truncate_cb(void *user_data)
{
int fd = *(int *)user_data;
off_t offset = lseek(fd, 0, SEEK_CUR);
return ftruncate(fd, offset);
}
-static void replace_tag(mp4ff_tag_t *tag, const char *new_val, bool *found)
-{
- free(tag->value);
- tag->value = para_strdup(new_val);
- *found = true;
-}
-
-static void add_tag(mp4ff_metadata_t *md, const char *item, const char *value)
+static void replace_or_add_tag(const char *item, const char *value,
+ struct mp4_metadata *meta)
{
- md->tags[md->count].item = para_strdup(item);
- md->tags[md->count].value = para_strdup(value);
- md->count++;
+ uint32_t n;
+ struct mp4_tag *t;
+
+ for (n = 0; n < meta->count; n++) {
+ t = meta->tags + n;
+ if (strcasecmp(t->item, item))
+ continue;
+ free(t->value);
+ t->value = para_strdup(value);
+ return;
+ }
+ /* item not found, add new tag */
+ meta->tags = para_realloc(meta->tags, (meta->count + 1)
+ * sizeof(struct mp4_tag));
+ t = meta->tags + meta->count;
+ t->item = para_strdup(item);
+ t->value = para_strdup(value);
+ meta->count++;
}
static int aac_afh_rewrite_tags(const char *map, size_t mapsize,
struct taginfo *tags, int fd, __a_unused const char *filename)
{
- int ret, i;
- int32_t rv;
- mp4ff_metadata_t metadata;
- mp4ff_t *mp4ff;
- mp4ff_callback_t cb = {
+ int ret;
+ struct mp4_metadata *metadata;
+ struct mp4 *mp4;
+ struct mp4_callback cb = {
.read = aac_afh_meta_read_cb,
.seek = aac_afh_meta_seek_cb,
.write = aac_afh_meta_write_cb,
.truncate = aac_afh_meta_truncate_cb,
.user_data = &fd
};
- bool found_artist = false, found_title = false, found_album = false,
- found_year = false, found_comment = false;
ret = write_all(fd, map, mapsize);
if (ret < 0)
return ret;
lseek(fd, 0, SEEK_SET);
- mp4ff = mp4ff_open_read_metaonly(&cb);
- if (!mp4ff)
- return -E_MP4FF_OPEN;
-
- ret = -E_MP4FF_META_READ;
- rv = mp4ff_meta_get_num_items(mp4ff);
- if (rv < 0)
- goto close;
- metadata.count = rv;
- PARA_NOTICE_LOG("%d metadata item(s) found\n", rv);
-
- metadata.tags = para_malloc((metadata.count + 5) * sizeof(mp4ff_tag_t));
- for (i = 0; i < metadata.count; i++) {
- mp4ff_tag_t *tag = metadata.tags + i;
-
- ret = -E_MP4FF_META_READ;
- if (!mp4ff_meta_get_by_index(mp4ff, i, &tag->item, &tag->value))
- goto free_tags;
- PARA_INFO_LOG("found: %s: %s\n", tag->item, tag->value);
- if (!strcmp(tag->item, "artist"))
- replace_tag(tag, tags->artist, &found_artist);
- else if (!strcmp(tag->item, "title"))
- replace_tag(tag, tags->title, &found_title);
- else if (!strcmp(tag->item, "album"))
- replace_tag(tag, tags->album, &found_album);
- else if (!strcmp(tag->item, "date"))
- replace_tag(tag, tags->year, &found_year);
- else if (!strcmp(tag->item, "comment"))
- replace_tag(tag, tags->comment, &found_comment);
- }
- if (!found_artist)
- add_tag(&metadata, "artist", tags->artist);
- if (!found_title)
- add_tag(&metadata, "title", tags->title);
- if (!found_album)
- add_tag(&metadata, "album", tags->album);
- if (!found_year)
- add_tag(&metadata, "date", tags->year);
- if (!found_comment)
- add_tag(&metadata, "comment", tags->comment);
- ret = -E_MP4FF_META_WRITE;
- if (!mp4ff_meta_update(&cb, &metadata))
- goto free_tags;
- ret = 1;
-free_tags:
- for (; i > 0; i--) {
- free(metadata.tags[i - 1].item);
- free(metadata.tags[i - 1].value);
- }
- free(metadata.tags);
-close:
- mp4ff_close(mp4ff);
+ ret = mp4_open_meta(&cb, &mp4);
+ if (ret < 0)
+ return ret;
+ metadata = mp4_get_meta(mp4);
+ PARA_NOTICE_LOG("%u metadata item(s) found\n", metadata->count);
+ replace_or_add_tag("artist", tags->artist, metadata);
+ replace_or_add_tag("title", tags->title, metadata);
+ replace_or_add_tag("album", tags->album, metadata);
+ replace_or_add_tag("date", tags->year, metadata);
+ replace_or_add_tag("comment", tags->comment, metadata);
+ ret = mp4_update_meta(mp4);
+ mp4_close(mp4);
return ret;
}
--- /dev/null
+/*
+ * 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 <regex.h>
+
+#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;
+}
+
+/** 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 */ \
+
+/** 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 = para_malloc(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 = para_malloc(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 = para_malloc(t->stsc_entry_count * sizeof(int32_t));
+ t->stsc_samples_per_chunk = para_malloc(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 = para_malloc(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 = para_malloc(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 = para_calloc(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 = para_malloc(*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;
+ 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.
+ */
+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;
+}