MP4 files may contain other atoms than "avcC" in the video track headers; so look for "avcC" and don't rely on it being the first one. Added limited support for edit lists in MP4/QuickTime files.

This commit is contained in:
Moritz Bunkus 2005-11-18 18:18:10 +00:00
parent ccb05d396f
commit be157d9fd3
5 changed files with 265 additions and 75 deletions

View File

@ -1,3 +1,12 @@
2005-11-18 Moritz Bunkus <moritz@bunkus.org>
* mkvmerge: new feature: Added limited support for edit lists in
MP4/QuickTime files. Fixes Anthill bug #151.
* mkvmerge: bug fix: MP4/QuickTime files which contain another
atom before the 'avcC' atom in the video track headers weren't
correctly remuxed.
2005-11-16 Moritz Bunkus <moritz@bunkus.org>
* mkvmerge: bug fix: mkvmerge will now refuse to append AVC/h.264

View File

@ -88,7 +88,11 @@ qtmp4_reader_c::probe_file(mm_io_c *in,
qtmp4_reader_c::qtmp4_reader_c(track_info_c &_ti)
throw (error_c):
generic_reader_c(_ti) {
generic_reader_c(_ti),
io(NULL), file_size(0), mdat_pos(-1), mdat_size(0),
time_scale(1), compression_algorithm(0),
main_dmx(-1) {
try {
io = new mm_file_io_c(ti.fname);
io->setFilePointer(0, seek_end);
@ -115,18 +119,21 @@ qtmp4_reader_c::~qtmp4_reader_c() {
}
qt_atom_t
qtmp4_reader_c::read_atom() {
qtmp4_reader_c::read_atom(mm_io_c *read_from) {
qt_atom_t a;
a.pos = io->getFilePointer();
a.size = io->read_uint32_be();
a.fourcc = io->read_uint32_be();
if (NULL == read_from)
read_from = io;
a.pos = read_from->getFilePointer();
a.size = read_from->read_uint32_be();
a.fourcc = read_from->read_uint32_be();
a.hsize = 8;
if (a.size == 1) {
a.size = io->read_uint64_be();
a.size = read_from->read_uint64_be();
a.hsize += 8;
} else if (a.size == 0)
a.size = file_size - io->getFilePointer() + 8;
a.size = file_size - read_from->getFilePointer() + 8;
if (a.size < a.hsize)
mxerror(PFX "Invalid chunk size " LLU " at " LLU ".\n", a.size, a.pos);
@ -145,7 +152,6 @@ qtmp4_reader_c::parse_headers() {
io->setFilePointer(0);
headers_parsed = false;
mdat_pos = -1;
do {
atom = read_atom();
mxverb(2, PFX "'%c%c%c%c' atom, size " LLD ", at " LLD "\n", BE2STR(atom),
@ -385,56 +391,129 @@ qtmp4_reader_c::update_tables(qtmp4_demuxer_ptr &dmx) {
}
mxverb(3, PFX "Frame offset table: %u entries\n",
(unsigned int)dmx->frame_offset_table.size());
update_editlist_table(dmx);
}
// Also taken from mplayer's demux_mov.c file.
void
qtmp4_reader_c::update_editlist_table(qtmp4_demuxer_ptr &dmx) {
if (dmx->editlist_table.empty())
return;
int frame = 0, e_pts = 0, i;
mxverb(4, "qtmp4: Updating edit list table for track %u\n", dmx->id);
for (i = 0; dmx->editlist_table.size() > i; ++i) {
qt_editlist_t &el = dmx->editlist_table[i];
int sample = 0, pts = el.pos;
el.start_frame = frame;
if (pts < 0) {
// skip!
el.frames = 0;
continue;
}
// find start sample
for (; dmx->sample_table.size() > sample; ++sample)
if (pts <= dmx->sample_table[sample].pts)
break;
el.start_sample = sample;
el.pts_offset = ((int64_t)e_pts * (int64_t)dmx->time_scale) /
(int64_t)time_scale - (int64_t)dmx->sample_table[sample].pts;
pts += ((int64_t)el.duration * (int64_t)dmx->time_scale) /
(int64_t)time_scale;
e_pts += el.duration;
// find end sample
for (; dmx->sample_table.size() > sample; ++sample)
if (pts <= dmx->sample_table[sample].pts)
break;
el.frames = sample - el.start_sample;
frame += el.frames;
mxverb(4, " %d: pts: %u 1st_sample: " LLU " frames: %u (%5.3fs) "
"pts_offset: " LLD "\n", i,
el.pos, el.start_sample, el.frames,
(float)(el.duration) / (float)time_scale, el.pts_offset);
}
}
void
qtmp4_reader_c::parse_header_priv_atoms(qtmp4_demuxer_ptr &dmx,
unsigned char *mem,
int size,
int level) {
qtmp4_reader_c::parse_video_header_priv_atoms(qtmp4_demuxer_ptr &dmx,
unsigned char *mem,
int size,
int level) {
mm_mem_io_c mio(mem, size);
try {
while (!mio.eof()) {
uint32_t add_atom, add_atom_size;
while (!mio.eof() && (mio.getFilePointer() < size)) {
qt_atom_t atom;
add_atom_size = mio.read_uint32_be();
add_atom = mio.read_uint32_be();
add_atom_size -= 8;
mxverb(2, PFX "%*s%s private data size: %u, type: "
"'%c%c%c%c'\n", (level + 1) * 2, "", dmx->type == 'a' ? "Audio" :
"Video", add_atom_size + 8, BE2STR(add_atom));
if (dmx->priv == NULL) {
uint32_t patom_size, patom;
atom = read_atom(&mio);
mxverb(2, PFX "%*sVideo private data size: %u, type: "
"'%c%c%c%c'\n", (level + 1) * 2, "", (unsigned int)atom.size,
BE2STR(atom.fourcc));
dmx->priv_size = add_atom_size;
dmx->priv = (unsigned char *)safemalloc(dmx->priv_size);
if (mio.read(dmx->priv, dmx->priv_size) != dmx->priv_size)
throw error_c("end-of-file");
mm_mem_io_c memio(dmx->priv, dmx->priv_size);
if (add_atom == FOURCC('e', 's', 'd', 's')) {
if (!dmx->esds_parsed)
dmx->esds_parsed = parse_esds_atom(memio, dmx, level + 1);
} else {
while (!memio.eof()) {
patom_size = memio.read_uint32_be();
if (patom_size <= 8)
break;
patom = memio.read_uint32_be();
mxverb(2, PFX "%*sAtom size: %u, atom: '%c%c%c%c'\n",
(level + 2) * 2, "", patom_size, BE2STR(patom));
if ((patom == FOURCC('e', 's', 'd', 's')) && !dmx->esds_parsed) {
memio.save_pos();
dmx->esds_parsed = parse_esds_atom(memio, dmx, level + 2);
memio.restore_pos();
}
memio.skip(patom_size - 8);
if ((FOURCC('e', 's', 'd', 's') == atom.fourcc) ||
(FOURCC('a', 'v', 'c', 'C') == atom.fourcc)) {
if (NULL == dmx->priv) {
dmx->priv_size = atom.size - atom.hsize;
dmx->priv = (unsigned char *)safemalloc(dmx->priv_size);
if (mio.read(dmx->priv, dmx->priv_size) != dmx->priv_size) {
safefree(dmx->priv);
dmx->priv = NULL;
dmx->priv_size = 0;
return;
}
}
} else
mio.skip(add_atom_size);
if ((FOURCC('e', 's', 'd', 's') == atom.fourcc) && !dmx->esds_parsed) {
mm_mem_io_c memio(dmx->priv, dmx->priv_size);
dmx->esds_parsed = parse_esds_atom(memio, dmx, level + 1);
}
}
mio.setFilePointer(atom.pos + atom.size);
}
} catch(...) {
}
}
void
qtmp4_reader_c::parse_audio_header_priv_atoms(qtmp4_demuxer_ptr &dmx,
unsigned char *mem,
int size,
int level) {
mm_mem_io_c mio(mem, size);
try {
while (!mio.eof() && (mio.getFilePointer() < (size - 8))) {
qt_atom_t atom;
atom = read_atom(&mio);
if (FOURCC('e', 's', 'd', 's') != atom.fourcc) {
mio.setFilePointer(atom.pos + 4);
continue;
}
mxverb(2, PFX "%*sAudio private data size: %u, type: "
"'%c%c%c%c'\n", (level + 1) * 2, "", (unsigned int)atom.size,
BE2STR(atom.fourcc));
if (!dmx->esds_parsed) {
mm_mem_io_c memio(mem + atom.pos + atom.hsize,
atom.size - atom.hsize);
dmx->esds_parsed = parse_esds_atom(memio, dmx, level + 1);
}
mio.setFilePointer(atom.pos + atom.size);
}
} catch(...) {
}
@ -606,7 +685,7 @@ qtmp4_reader_c::handle_mdhd_atom(qtmp4_demuxer_ptr &new_dmx,
mxverb(2, PFX "%*s Time scale: %u, duration: %u\n", level * 2, "",
get_uint32_be(&mdhd.time_scale),
get_uint32_be(&mdhd.duration));
new_dmx->timescale = get_uint32_be(&mdhd.time_scale);
new_dmx->time_scale = get_uint32_be(&mdhd.time_scale);
new_dmx->global_duration = get_uint32_be(&mdhd.duration);
}
@ -699,8 +778,8 @@ qtmp4_reader_c::handle_mvhd_atom(qt_atom_t atom,
"size: " LLD ".\n", (unsigned int)sizeof(mvhd_atom_t), atom.size);
if (io->read(&mvhd, sizeof(mvhd_atom_t)) != sizeof(mvhd_atom_t))
throw error_c("end-of-file");
mxverb(2, PFX "%*s Time scale: %u\n", level * 2, "",
get_uint32_be(&mvhd.time_scale));
time_scale = get_uint32_be(&mvhd.time_scale);
mxverb(2, PFX "%*s Time scale: %u\n", level * 2, "", time_scale);
}
void
@ -903,9 +982,14 @@ qtmp4_reader_c::handle_stsd_atom(qtmp4_demuxer_ptr &new_dmx,
stsd_size = sizeof(video_stsd_atom_t);
}
if ((stsd_size > 0) && (stsd_size < size))
parse_header_priv_atoms(new_dmx, priv + stsd_size, size -
stsd_size, level);
if ((stsd_size > 0) && (stsd_size < size)) {
if ('v' == new_dmx->type)
parse_video_header_priv_atoms(new_dmx, priv + stsd_size, size -
stsd_size, level);
else if ('a' == new_dmx->type)
parse_audio_header_priv_atoms(new_dmx, priv + stsd_size, size -
stsd_size, level);
}
safefree(priv);
io->setFilePointer(pos + size);
@ -996,6 +1080,50 @@ qtmp4_reader_c::handle_stts_atom(qtmp4_demuxer_ptr &new_dmx,
count);
}
void
qtmp4_reader_c::handle_edts_atom(qtmp4_demuxer_ptr &new_dmx,
qt_atom_t parent,
int level) {
while (parent.size > 0) {
qt_atom_t atom;
atom = read_atom();
mxverb(2, PFX "%*s'%c%c%c%c' atom, size " LLD ", at " LLD "\n", 2 * level,
"", BE2STR(atom.fourcc), atom.size, atom.pos);
if (atom.fourcc == FOURCC('e', 'l', 's', 't'))
handle_elst_atom(new_dmx, atom.to_parent(), level + 1);
skip_atom();
parent.size -= atom.size;
}
}
void
qtmp4_reader_c::handle_elst_atom(qtmp4_demuxer_ptr &new_dmx,
qt_atom_t atom,
int level) {
uint32_t count, i;
io->skip(1 + 3); // version & flags
count = io->read_uint32_be();
new_dmx->editlist_table.resize(count);
for (i = 0; i < count; i++) {
qt_editlist_t &editlist = new_dmx->editlist_table[i];
editlist.duration = io->read_uint32_be();
editlist.pos = io->read_uint32_be();
editlist.speed = io->read_uint32_be();
}
mxverb(2, PFX "%*sEdit list table: %u entries\n", level * 2, "",
count);
for (i = 0; i < count; ++i)
mxverb(4, PFX "%*s%u: duration %u pos %u speed %u\n", (level + 1) * 2, "",
i, new_dmx->editlist_table[i].duration,
new_dmx->editlist_table[i].pos, new_dmx->editlist_table[i].speed);
}
void
qtmp4_reader_c::handle_tkhd_atom(qtmp4_demuxer_ptr &new_dmx,
qt_atom_t atom,
@ -1029,6 +1157,9 @@ qtmp4_reader_c::handle_trak_atom(qtmp4_demuxer_ptr &new_dmx,
else if (atom.fourcc == FOURCC('m', 'd', 'i', 'a'))
handle_mdia_atom(new_dmx, atom.to_parent(), level + 1);
else if (atom.fourcc == FOURCC('e', 'd', 't', 's'))
handle_edts_atom(new_dmx, atom.to_parent(), level + 1);
skip_atom();
parent.size -= atom.size;
}
@ -1049,11 +1180,11 @@ qtmp4_reader_c::handle_video_with_bframes(qtmp4_demuxer_ptr &dmx,
if (dmx->pos == 0)
dmx->v_dts_offset = (int64_t)dmx->frame_offset_table[0] *
1000000000 / dmx->timescale;
1000000000 / dmx->time_scale;
old_timecode = timecode;
timecode += (int64_t)dmx->frame_offset_table[dmx->pos] *
1000000000 / dmx->timescale - dmx->v_dts_offset;
1000000000 / dmx->time_scale - dmx->v_dts_offset;
PTZR(dmx->ptzr)->process(new packet_t(mem, timecode, duration, bref, fref));
}
@ -1081,7 +1212,7 @@ qtmp4_reader_c::read(generic_packetizer_c *ptzr,
io->setFilePointer(dmx->chunk_table[dmx->pos].pos);
timecode = 1000000000 *
((uint64_t)dmx->chunk_table[dmx->pos].samples *
(uint64_t)dmx->duration) / (uint64_t)dmx->timescale;
(uint64_t)dmx->duration) / (uint64_t)dmx->time_scale;
if (dmx->sample_size != 1) {
if (!dmx->warning_printed) {
@ -1130,7 +1261,7 @@ qtmp4_reader_c::read(generic_packetizer_c *ptzr,
duration = 1000000000 *
((uint64_t)dmx->chunk_table[dmx->pos + 1].samples *
(uint64_t)dmx->duration) /
(uint64_t)dmx->timescale - timecode;
(uint64_t)dmx->time_scale - timecode;
else
duration = dmx->avg_duration;
dmx->avg_duration = (dmx->avg_duration * dmx->pos + duration) /
@ -1152,11 +1283,37 @@ qtmp4_reader_c::read(generic_packetizer_c *ptzr,
frame = dmx->pos;
timecode = (int64_t)dmx->sample_table[frame].pts * 1000000000 /
dmx->timescale;
if (('v' == dmx->type) && !dmx->editlist_table.empty()) {
// find the right editlist_table entry:
if (frame < dmx->editlist_table[dmx->editlist_pos].start_frame)
dmx->editlist_pos = 0;
while (((dmx->editlist_table.size() - 1) > dmx->editlist_pos) &&
(frame >=
dmx->editlist_table[dmx->editlist_pos + 1].start_frame))
++dmx->editlist_pos;
if ((dmx->editlist_table[dmx->editlist_pos].start_frame +
dmx->editlist_table[dmx->editlist_pos].frames) <= frame)
continue; // EOF
// calc real frame index:
frame -= dmx->editlist_table[dmx->editlist_pos].start_frame;
frame += dmx->editlist_table[dmx->editlist_pos].start_sample;
// calc pts:
timecode =
(dmx->sample_table[frame].pts +
dmx->editlist_table[dmx->editlist_pos].pts_offset) *
1000000000ll / dmx->time_scale;
} else
timecode = (int64_t)dmx->sample_table[frame].pts * 1000000000 /
dmx->time_scale;
if ((frame + 1) < dmx->sample_table.size())
duration = (int64_t)dmx->sample_table[frame + 1].pts * 1000000000 /
dmx->timescale - timecode;
dmx->time_scale - timecode;
else
duration = dmx->avg_duration;
dmx->avg_duration = (dmx->avg_duration * frame + duration) /
@ -1167,7 +1324,7 @@ qtmp4_reader_c::read(generic_packetizer_c *ptzr,
else {
is_keyframe = false;
for (k = 0; k < dmx->keyframe_table.size(); k++)
if (dmx->keyframe_table[k] == (frame + 1)) {
if (dmx->keyframe_table[k] == (dmx->pos + 1)) {
is_keyframe = true;
break;
}
@ -1574,7 +1731,7 @@ qtmp4_demuxer_t::calculate_fps() {
if ((1 == durmap_table.size()) && (0 != durmap_table[0].duration) &&
((0 != sample_size) || (0 == frame_offset_table.size()))) {
// Constant FPS. Let's set the default duration.
fps = (double)timescale / (double)durmap_table[0].duration;
fps = (double)time_scale / (double)durmap_table[0].duration;
mxverb(3, PFX "calculate_fps: case 1: %f\n", fps);
return fps;
}
@ -1594,7 +1751,7 @@ qtmp4_demuxer_t::calculate_fps() {
int64_t timecode;
timecode = ((int64_t)sample_table[i].pts +
(int64_t)frame_offset_table[pos]) * 1000000000 / timescale;
(int64_t)frame_offset_table[pos]) * 1000000000 / time_scale;
if (timecode > max_tc)
max_tc = timecode;
}

View File

@ -60,9 +60,9 @@ struct qt_editlist_t {
uint32_t pos;
uint32_t speed;
uint32_t frames;
uint32_t start_sample;
uint32_t start_frame;
uint32_t pts_offset;
uint64_t start_sample;
uint64_t start_frame;
int64_t pts_offset;
qt_editlist_t():
duration(0), pos(0), speed(0), frames(0),
@ -94,7 +94,7 @@ struct qtmp4_demuxer_t {
char fourcc[4];
uint32_t pos;
uint32_t timescale;
uint32_t time_scale;
uint32_t global_duration;
uint32_t avg_duration;
uint32_t sample_size;
@ -110,6 +110,8 @@ struct qtmp4_demuxer_t {
vector<qt_frame_offset_t> raw_frame_offset_table;
vector<int32_t> frame_offset_table;
int editlist_pos;
esds_t esds;
bool esds_parsed;
@ -133,9 +135,9 @@ struct qtmp4_demuxer_t {
qtmp4_demuxer_t():
ok(false), type('?'), id(0), pos(0),
timescale(1), global_duration(0), avg_duration(0), sample_size(0),
time_scale(1), global_duration(0), avg_duration(0), sample_size(0),
duration(0),
esds_parsed(false),
editlist_pos(0), esds_parsed(false),
v_stsd(NULL), v_stsd_size(0),
v_width(0), v_height(0), v_bitdepth(0), v_dts_offset(0),
avc_use_bframes(false),
@ -185,7 +187,7 @@ private:
mm_io_c *io;
vector<qtmp4_demuxer_ptr> demuxers;
int64_t file_size, mdat_pos, mdat_size;
uint32_t compression_algorithm;
uint32_t time_scale, compression_algorithm;
int main_dmx;
public:
@ -203,10 +205,13 @@ public:
protected:
virtual void parse_headers();
virtual qt_atom_t read_atom();
virtual void parse_header_priv_atoms(qtmp4_demuxer_ptr &dmx,
unsigned char *mem, int size,
int level);
virtual qt_atom_t read_atom(mm_io_c *read_from = NULL);
virtual void parse_video_header_priv_atoms(qtmp4_demuxer_ptr &dmx,
unsigned char *mem, int size,
int level);
virtual void parse_audio_header_priv_atoms(qtmp4_demuxer_ptr &dmx,
unsigned char *mem, int size,
int level);
virtual bool parse_esds_atom(mm_mem_io_c &memio, qtmp4_demuxer_ptr &dmx,
int level);
virtual uint32_t read_esds_descr_len(mm_mem_io_c &memio);
@ -253,8 +258,13 @@ protected:
int level);
virtual void handle_trak_atom(qtmp4_demuxer_ptr &new_dmx, qt_atom_t parent,
int level);
virtual void handle_edts_atom(qtmp4_demuxer_ptr &new_dmx, qt_atom_t parent,
int level);
virtual void handle_elst_atom(qtmp4_demuxer_ptr &new_dmx, qt_atom_t parent,
int level);
virtual void update_tables(qtmp4_demuxer_ptr &dmx);
virtual void update_editlist_table(qtmp4_demuxer_ptr &dmx);
};
#endif // __R_QTMP4_H

View File

@ -62,3 +62,4 @@ T_212ssa_attachments:7555c28c8b18536880248bb88253fa49-9a5a5ed80808c9d448ca5b44b6
T_213mp4_broken_pixel_dimensions:aad69e63948415b4b6407d493005b774:passed:20050919-094831
T_214one_frame_avi:bea975277e91ded1a4085043e89608ce:passed:20051004-192755
T_215X_codec_extradata_avi:78d669fc6511b6b931cd0981e2e1cca9-3fabc505fdd377c05ce4557b1bc13545:passed:20051004-194707
T_216mp4_editlists:30ba8cadde2b266917ab105f5c754cb7:passed:20051118-191453

View File

@ -0,0 +1,13 @@
#!/usr/bin/ruby -w
class T_216mp4_editlists < Test
def description
return "mkvmerge / edit lists in MP4 and 'colr' before 'avcC' / in(MP4)"
end
def run
merge("-A -S data/mp4/oops.mov")
return hash_tmp
end
end