/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // // Heavily based on ffmpeg code. // // Copyright (c) 2001 Fabrice Bellard. // First version by Francois Revol revol@free.fr // Seek function by Gael Chardon gael.dev@4now.net // #include "common/debug.h" #include "common/endian.h" #include "common/macresman.h" #include "common/memstream.h" #include "common/formats/quicktime.h" #include "common/textconsole.h" #include "common/util.h" #include "common/compression/deflate.h" namespace Common { //////////////////////////////////////////// // QuickTimeParser //////////////////////////////////////////// QuickTimeParser::QuickTimeParser() { _beginOffset = 0; _fd = nullptr; _scaleFactorX = 1; _scaleFactorY = 1; _resFork = new MacResManager(); _disposeFileHandle = DisposeAfterUse::YES; _timeScale = 1; _qtvrType = QTVRType::OTHER; _winX = 0; _winY = 0; _panoTrack = nullptr; initParseTable(); } QuickTimeParser::~QuickTimeParser() { close(); delete _resFork; } bool QuickTimeParser::parseFile(const Path &filename) { if (!_resFork->open(filename)) return false; _foundMOOV = false; _disposeFileHandle = DisposeAfterUse::YES; Atom atom = { 0, 0, 0 }; if (_resFork->hasResFork()) { // Search for a 'moov' resource MacResIDArray idArray = _resFork->getResIDArray(MKTAG('m', 'o', 'o', 'v')); if (!idArray.empty()) _fd = _resFork->getResource(MKTAG('m', 'o', 'o', 'v'), idArray[0]); if (_fd) { atom.size = _fd->size(); if (readDefault(atom) < 0 || !_foundMOOV) return false; } delete _fd; } _fd = Common::MacResManager::openFileOrDataFork(filename); if (!_fd) return false; atom.size = _fd->size(); if (readDefault(atom) < 0 || !_foundMOOV) return false; if (_qtvrType == QTVRType::PANORAMA) { if (!parsePanoramaAtoms()) return false; } init(); return true; } bool QuickTimeParser::parseStream(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle) { _fd = stream; _foundMOOV = false; _disposeFileHandle = disposeFileHandle; Atom atom = { 0, 0, 0xffffffff }; if (readDefault(atom) < 0 || !_foundMOOV) { close(); return false; } if (_qtvrType == QTVRType::PANORAMA) { if (!parsePanoramaAtoms()) return false; } init(); return true; } bool QuickTimeParser::parsePanoramaAtoms() { for (uint i = 0; i < _tracks.size(); i++) { if (_tracks[i]->codecType == CODEC_TYPE_PANO) { _panoTrack = _tracks[i]; break; } } if (!_panoTrack) { warning("QuickTimeParser::parsePanoramaAtoms(): No panoramic track found"); return false; } Array sizes; if (_panoTrack->sampleSize) { sizes = { _panoTrack->sampleSize }; } else { sizes.resize(_panoTrack->sampleCount); for (uint32 i = 0; i < _panoTrack->sampleCount; i++) sizes[i] = _panoTrack->sampleSizes[i]; } for (uint32 i = 0; i < sizes.size() && i < _panoTrack->chunkCount; i++) { _panoTrack->panoSamples.resize(_panoTrack->panoSamples.size() + 1); Atom atom = { 0, _panoTrack->chunkOffsets[i], sizes[i] }; _fd->seek(_panoTrack->chunkOffsets[i], SEEK_SET); if (readDefault(atom) < 0) return false; } return true; } void QuickTimeParser::init() { for (uint32 i = 0; i < _tracks.size(); i++) { // Remove unknown/unhandled tracks if (_tracks[i]->codecType == CODEC_TYPE_MOV_OTHER) { delete _tracks[i]; _tracks.remove_at(i); i--; } else { // If this track doesn't have a declared scale, use the movie scale if (_tracks[i]->timeScale == 0) _tracks[i]->timeScale = _timeScale; // If this track doesn't have an edit list (like in MPEG-4 files), // fake an entry of one edit that takes up the entire sample if (_tracks[i]->editList.size() == 0) { _tracks[i]->editList.resize(1); _tracks[i]->editList[0].trackDuration = _tracks[i]->duration; _tracks[i]->editList[0].timeOffset = 0; _tracks[i]->editList[0].mediaTime = 0; _tracks[i]->editList[0].mediaRate = 1; } } } } void QuickTimeParser::initParseTable() { static const ParseTable p[] = { { &QuickTimeParser::readDefault, MKTAG('d', 'i', 'n', 'f') }, { &QuickTimeParser::readDREF, MKTAG('d', 'r', 'e', 'f') }, { &QuickTimeParser::readDefault, MKTAG('e', 'd', 't', 's') }, { &QuickTimeParser::readELST, MKTAG('e', 'l', 's', 't') }, { &QuickTimeParser::readHDLR, MKTAG('h', 'd', 'l', 'r') }, { &QuickTimeParser::readLeaf, MKTAG('m', 'd', 'a', 't') }, { &QuickTimeParser::readMDHD, MKTAG('m', 'd', 'h', 'd') }, { &QuickTimeParser::readDefault, MKTAG('m', 'd', 'i', 'a') }, { &QuickTimeParser::readDefault, MKTAG('m', 'i', 'n', 'f') }, { &QuickTimeParser::readMOOV, MKTAG('m', 'o', 'o', 'v') }, { &QuickTimeParser::readMVHD, MKTAG('m', 'v', 'h', 'd') }, { &QuickTimeParser::readSMHD, MKTAG('s', 'm', 'h', 'd') }, { &QuickTimeParser::readDefault, MKTAG('s', 't', 'b', 'l') }, { &QuickTimeParser::readSTCO, MKTAG('s', 't', 'c', 'o') }, { &QuickTimeParser::readSTSC, MKTAG('s', 't', 's', 'c') }, { &QuickTimeParser::readSTSD, MKTAG('s', 't', 's', 'd') }, { &QuickTimeParser::readSTSS, MKTAG('s', 't', 's', 's') }, { &QuickTimeParser::readSTSZ, MKTAG('s', 't', 's', 'z') }, { &QuickTimeParser::readSTTS, MKTAG('s', 't', 't', 's') }, { &QuickTimeParser::readTKHD, MKTAG('t', 'k', 'h', 'd') }, { &QuickTimeParser::readTRAK, MKTAG('t', 'r', 'a', 'k') }, { &QuickTimeParser::readDefault, MKTAG('u', 'd', 't', 'a') }, { &QuickTimeParser::readCTYP, MKTAG('c', 't', 'y', 'p') }, { &QuickTimeParser::readWLOC, MKTAG('W', 'L', 'O', 'C') }, { &QuickTimeParser::readNAVG, MKTAG('N', 'A', 'V', 'G') }, { &QuickTimeParser::readVMHD, MKTAG('v', 'm', 'h', 'd') }, { &QuickTimeParser::readCMOV, MKTAG('c', 'm', 'o', 'v') }, { &QuickTimeParser::readWAVE, MKTAG('w', 'a', 'v', 'e') }, { &QuickTimeParser::readESDS, MKTAG('e', 's', 'd', 's') }, { &QuickTimeParser::readSMI, MKTAG('S', 'M', 'I', ' ') }, { &QuickTimeParser::readDefault, MKTAG('g', 'm', 'h', 'd') }, { &QuickTimeParser::readGMIN, MKTAG('g', 'm', 'i', 'n') }, { &QuickTimeParser::readDefault, MKTAG('S', 'T', 'p', 'n') }, { &QuickTimeParser::readPINF, MKTAG('p', 'I', 'n', 'f') }, { &QuickTimeParser::readDefault, MKTAG('S', 'T', 'p', 'n') }, { &QuickTimeParser::readPHDR, MKTAG('p', 'H', 'd', 'r') }, { &QuickTimeParser::readPHOT, MKTAG('p', 'H', 'o', 't') }, { &QuickTimeParser::readSTRT, MKTAG('s', 't', 'r', 'T') }, { &QuickTimeParser::readPLNK, MKTAG('p', 'L', 'n', 'k') }, { &QuickTimeParser::readPNAV, MKTAG('p', 'N', 'a', 'v') }, { nullptr, 0 } }; _parseTable = p; } int QuickTimeParser::readDefault(Atom atom) { uint32 total_size = 0; Atom a; int err = 0; a.offset = atom.offset; if (_fd->eos() || _fd->err() || (_fd->pos() == _fd->size())) return -1; while(((total_size + 8) < atom.size) && !_fd->eos() && _fd->pos() < _fd->size() && !err) { a.size = atom.size; a.type = 0; if (atom.size >= 8) { a.size = _fd->readUint32BE(); a.type = _fd->readUint32BE(); // Some QuickTime videos with resource forks have mdat chunks // that are of size 0. Adjust it so it's the correct size. if (a.type == MKTAG('m', 'd', 'a', 't') && a.size == 0) a.size = _fd->size(); } total_size += 8; a.offset += 8; debugC(4, kDebugLevelGVideo, "type: %08x %.4s sz: %x %x %x", a.type, tag2str(a.type), a.size, atom.size, total_size); if (a.size == 1) { // 64 bit extended size warning("64 bit extended size is not supported in QuickTime"); return -1; } if (a.size == 0) { a.size = atom.size - total_size; if (a.size <= 8) break; } uint32 i = 0; for (; _parseTable[i].type != 0 && _parseTable[i].type != a.type; i++) ; // Empty if (a.size < 8) break; a.size -= 8; if (a.size + (uint32)_fd->pos() > (uint32)_fd->size()) { _fd->seek(_fd->size()); debugC(0, kDebugLevelGVideo, "Skipping junk found at the end of the QuickTime file"); return 0; } else if (_parseTable[i].type == 0) { // skip leaf atom data debugC(0, kDebugLevelGVideo, ">>> Skipped [%s] (%08x) at %d (0x%x)", tag2str(a.type), a.type, (uint32)_fd->pos(), (uint32)_fd->pos()); _fd->seek(a.size, SEEK_CUR); } else { uint32 start_pos = _fd->pos(); err = (this->*_parseTable[i].func)(a); if (!err && (_fd->eos() || _fd->err())) err = -1; uint32 left = a.size - _fd->pos() + start_pos; if (left > 0) // skip garbage at atom end _fd->seek(left, SEEK_CUR); } a.offset += a.size; total_size += a.size; } if (!err && total_size < atom.size) _fd->seek(atom.size - total_size, SEEK_SET); return err; } int QuickTimeParser::readLeaf(Atom atom) { if (atom.size > 1) _fd->seek(atom.size, SEEK_SET); return 0; } int QuickTimeParser::readMOOV(Atom atom) { if (readDefault(atom) < 0) return -1; // We parsed the 'moov' atom, so we don't need anything else _foundMOOV = true; return 1; } int QuickTimeParser::readCMOV(Atom atom) { // Read in the dcom atom _fd->readUint32BE(); if (_fd->readUint32BE() != MKTAG('d', 'c', 'o', 'm')) return -1; if (_fd->readUint32BE() != MKTAG('z', 'l', 'i', 'b')) { warning("Unknown cmov compression type"); return -1; } // Read in the cmvd atom uint32 compressedSize = _fd->readUint32BE() - 12; if (_fd->readUint32BE() != MKTAG('c', 'm', 'v', 'd')) return -1; uint32 uncompressedSize = _fd->readUint32BE(); // Read in data byte *compressedData = (byte *)malloc(compressedSize); _fd->read(compressedData, compressedSize); // Create uncompressed stream byte *uncompressedData = (byte *)malloc(uncompressedSize); // Uncompress the data unsigned long dstLen = uncompressedSize; if (!inflateZlib(uncompressedData, &dstLen, compressedData, compressedSize)) { warning ("Could not uncompress cmov chunk"); free(compressedData); free(uncompressedData); return -1; } // Load data into a new MemoryReadStream and assign _fd to be that SeekableReadStream *oldStream = _fd; _fd = new MemoryReadStream(uncompressedData, uncompressedSize, DisposeAfterUse::YES); // Read the contents of the uncompressed data Atom a = { MKTAG('m', 'o', 'o', 'v'), 0, uncompressedSize }; int err = readDefault(a); // Assign the file handle back to the original handle free(compressedData); delete _fd; _fd = oldStream; return err; } int QuickTimeParser::readMVHD(Atom atom) { byte version = _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags if (version == 1) { warning("QuickTime version 1"); _fd->readUint32BE(); _fd->readUint32BE(); _fd->readUint32BE(); _fd->readUint32BE(); } else { _fd->readUint32BE(); // creation time _fd->readUint32BE(); // modification time } _timeScale = _fd->readUint32BE(); // time scale debugC(0, kDebugLevelGVideo, "time scale = %i\n", _timeScale); // duration _duration = (version == 1) ? (_fd->readUint32BE(), _fd->readUint32BE()) : _fd->readUint32BE(); _fd->readUint32BE(); // preferred scale _fd->readUint16BE(); // preferred volume _fd->seek(10, SEEK_CUR); // reserved // We only need two values from the movie display matrix. Most of the values are just // skipped. xMod and yMod are 16:16 fixed point numbers, the last part of the 3x3 matrix // is 2:30. uint32 xMod = _fd->readUint32BE(); _fd->skip(12); uint32 yMod = _fd->readUint32BE(); _fd->skip(16); _scaleFactorX = Rational(0x10000, xMod); _scaleFactorY = Rational(0x10000, yMod); _scaleFactorX.debugPrintC(1, kDebugLevelGVideo, "readMVHD(): scaleFactorX ="); _scaleFactorY.debugPrintC(1, kDebugLevelGVideo, "readMVHD(): scaleFactorY ="); _fd->readUint32BE(); // preview time _fd->readUint32BE(); // preview duration _fd->readUint32BE(); // poster time _fd->readUint32BE(); // selection time _fd->readUint32BE(); // selection duration _fd->readUint32BE(); // current time _fd->readUint32BE(); // next track ID return 0; } int QuickTimeParser::readTRAK(Atom atom) { Track *track = new Track(); track->codecType = CODEC_TYPE_MOV_OTHER; _tracks.push_back(track); return readDefault(atom); } int QuickTimeParser::readSMHD(Atom atom) { Track *track = _tracks.back(); _fd->readUint32BE(); // version + flags track->soundBalance = _fd->readUint16BE(); _fd->readUint16BE(); // reserved return 0; } int QuickTimeParser::readTKHD(Atom atom) { Track *track = _tracks.back(); byte version = _fd->readByte(); _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags // //MOV_TRACK_ENABLED 0x0001 //MOV_TRACK_IN_MOVIE 0x0002 //MOV_TRACK_IN_PREVIEW 0x0004 //MOV_TRACK_IN_POSTER 0x0008 // if (version == 1) { _fd->readUint32BE(); _fd->readUint32BE(); _fd->readUint32BE(); _fd->readUint32BE(); } else { _fd->readUint32BE(); // creation time _fd->readUint32BE(); // modification time } /* track->id = */_fd->readUint32BE(); // track id (NOT 0 !) _fd->readUint32BE(); // reserved track->duration = (version == 1) ? (_fd->readUint32BE(), _fd->readUint32BE()) : _fd->readUint32BE(); // highlevel (considering edits) duration in movie timebase _fd->readUint32BE(); // reserved _fd->readUint32BE(); // reserved _fd->readUint16BE(); // layer _fd->readUint16BE(); // alternate group _fd->readUint16BE(); // volume _fd->readUint16BE(); // reserved // We only need the two values from the displacement matrix for a track. // See readMVHD() for more information. uint32 xMod = _fd->readUint32BE(); _fd->skip(12); uint32 yMod = _fd->readUint32BE(); _fd->skip(16); track->scaleFactorX = Rational(0x10000, xMod); track->scaleFactorY = Rational(0x10000, yMod); track->scaleFactorX.debugPrintC(1, kDebugLevelGVideo, "readTKHD(): scaleFactorX ="); track->scaleFactorY.debugPrintC(1, kDebugLevelGVideo, "readTKHD(): scaleFactorY ="); // these are fixed-point, 16:16 //_fd->readUint32BE() >> 16; // track width //_fd->readUint32BE() >> 16; // track height return 0; } // edit list atom int QuickTimeParser::readELST(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags uint32 editCount = _fd->readUint32BE(); track->editList.resize(editCount); debugC(2, kDebugLevelGVideo, "Track %d edit list count: %d", _tracks.size() - 1, editCount); uint32 offset = 0; for (uint32 i = 0; i < editCount; i++) { track->editList[i].trackDuration = _fd->readUint32BE(); track->editList[i].mediaTime = _fd->readSint32BE(); track->editList[i].mediaRate = Rational(_fd->readUint32BE(), 0x10000); track->editList[i].timeOffset = offset; debugCN(3, kDebugLevelGVideo, "\tDuration = %d (Offset = %d), Media Time = %d, ", track->editList[i].trackDuration, offset, track->editList[i].mediaTime); track->editList[i].mediaRate.debugPrintC(3, kDebugLevelGVideo, "Media Rate ="); offset += track->editList[i].trackDuration; } return 0; } int QuickTimeParser::readHDLR(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags // component type uint32 ctype = _fd->readUint32BE(); uint32 type = _fd->readUint32BE(); // component subtype debugC(0, kDebugLevelGVideo, "ctype= %s (0x%08lx)", tag2str(ctype), (long)ctype); debugC(0, kDebugLevelGVideo, "stype= %s", tag2str(type)); if (ctype == MKTAG('m', 'h', 'l', 'r')) // MOV debugC(0, kDebugLevelGVideo, "MOV detected"); else if (ctype == 0) debugC(0, kDebugLevelGVideo, "MPEG-4 detected"); if (type == MKTAG('v', 'i', 'd', 'e')) track->codecType = CODEC_TYPE_VIDEO; else if (type == MKTAG('s', 'o', 'u', 'n')) track->codecType = CODEC_TYPE_AUDIO; else if (type == MKTAG('m', 'u', 's', 'i')) track->codecType = CODEC_TYPE_MIDI; else if (type == MKTAG('S', 'T', 'p', 'n') || type == MKTAG('s', 't', 'p', 'n')) track->codecType = CODEC_TYPE_PANO; _fd->readUint32BE(); // component manufacture _fd->readUint32BE(); // component flags _fd->readUint32BE(); // component flags mask if (atom.size <= 24) return 0; // nothing left to read // .mov: PASCAL string byte len = _fd->readByte(); _fd->seek(len, SEEK_CUR); _fd->seek(atom.size - (_fd->pos() - atom.offset), SEEK_CUR); return 0; } int QuickTimeParser::readMDHD(Atom atom) { Track *track = _tracks.back(); byte version = _fd->readByte(); if (version > 1) return 1; // unsupported _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags if (version == 1) { _fd->readUint32BE(); _fd->readUint32BE(); _fd->readUint32BE(); _fd->readUint32BE(); } else { _fd->readUint32BE(); // creation time _fd->readUint32BE(); // modification time } track->timeScale = _fd->readUint32BE(); track->mediaDuration = (version == 1) ? (_fd->readUint32BE(), _fd->readUint32BE()) : _fd->readUint32BE(); // duration _fd->readUint16BE(); // language _fd->readUint16BE(); // quality return 0; } int QuickTimeParser::readSTSD(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags uint32 entryCount = _fd->readUint32BE(); track->sampleDescs.reserve(entryCount); for (uint32 i = 0; i < entryCount; i++) { // Parsing Sample description table Atom a = { 0, 0, 0 }; uint32 start_pos = _fd->pos(); int size = _fd->readUint32BE(); // size uint32 format = _fd->readUint32BE(); // data format _fd->readUint32BE(); // reserved _fd->readUint16BE(); // reserved _fd->readUint16BE(); // index debugC(0, kDebugLevelGVideo, "sampledesc %d: size=%d 4CC= %s codec_type=%d", i, size, tag2str(format), track->codecType); track->sampleDescs.push_back(readSampleDesc(track, format, size - 16)); if (!track->sampleDescs[i]) { // other codec type, just skip (rtp, mp4s, tmcd ...) _fd->seek(size - (_fd->pos() - start_pos), SEEK_CUR); } // this will read extra atoms at the end (wave, alac, damr, avcC, SMI ...) a.size = size - (_fd->pos() - start_pos); if (a.size > 8) readDefault(a); else if (a.size > 0) _fd->seek(a.size, SEEK_CUR); } return 0; } int QuickTimeParser::readSTSC(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags track->sampleToChunkCount = _fd->readUint32BE(); debugC(0, kDebugLevelGVideo, "track[%i].stsc.entries = %i", _tracks.size() - 1, track->sampleToChunkCount); track->sampleToChunk = new SampleToChunkEntry[track->sampleToChunkCount]; if (!track->sampleToChunk) return -1; for (uint32 i = 0; i < track->sampleToChunkCount; i++) { track->sampleToChunk[i].first = _fd->readUint32BE() - 1; track->sampleToChunk[i].count = _fd->readUint32BE(); track->sampleToChunk[i].id = _fd->readUint32BE(); //warning("Sample to Chunk[%d]: First = %d, Count = %d", i, track->sampleToChunk[i].first, track->sampleToChunk[i].count); } return 0; } int QuickTimeParser::readSTSS(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags track->keyframeCount = _fd->readUint32BE(); debugC(0, kDebugLevelGVideo, "keyframeCount = %d", track->keyframeCount); track->keyframes = new uint32[track->keyframeCount]; if (!track->keyframes) return -1; for (uint32 i = 0; i < track->keyframeCount; i++) { track->keyframes[i] = _fd->readUint32BE() - 1; // Adjust here, the frames are based on 1 debugC(6, kDebugLevelGVideo, "keyframes[%d] = %d", i, track->keyframes[i]); } return 0; } int QuickTimeParser::readSTSZ(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags track->sampleSize = _fd->readUint32BE(); track->sampleCount = _fd->readUint32BE(); debugC(5, kDebugLevelGVideo, "sampleSize = %d sampleCount = %d", track->sampleSize, track->sampleCount); if (track->sampleSize) return 0; // there isn't any table following track->sampleSizes = new uint32[track->sampleCount]; if (!track->sampleSizes) return -1; for(uint32 i = 0; i < track->sampleCount; i++) { track->sampleSizes[i] = _fd->readUint32BE(); debugC(6, kDebugLevelGVideo, "sampleSizes[%d] = %d", i, track->sampleSizes[i]); } return 0; } int QuickTimeParser::readSTTS(Atom atom) { Track *track = _tracks.back(); uint32 totalSampleCount = 0; _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags track->timeToSampleCount = _fd->readUint32BE(); track->timeToSample = new TimeToSampleEntry[track->timeToSampleCount]; debugC(0, kDebugLevelGVideo, "track[%d].stts.entries = %d", _tracks.size() - 1, track->timeToSampleCount); for (int32 i = 0; i < track->timeToSampleCount; i++) { track->timeToSample[i].count = _fd->readUint32BE(); track->timeToSample[i].duration = _fd->readUint32BE(); debugC(1, kDebugLevelGVideo, "\tCount = %d, Duration = %d", track->timeToSample[i].count, track->timeToSample[i].duration); totalSampleCount += track->timeToSample[i].count; } track->frameCount = totalSampleCount; return 0; } int QuickTimeParser::readVMHD(Atom atom) { Track *track = _tracks.back(); _fd->readUint32BE(); // version + flags track->graphicsMode = (GraphicsMode)_fd->readUint16BE(); _fd->readMultipleBE(track->opcolor); return 0; } int QuickTimeParser::readSTCO(Atom atom) { Track *track = _tracks.back(); _fd->readByte(); // version _fd->readByte(); _fd->readByte(); _fd->readByte(); // flags track->chunkCount = _fd->readUint32BE(); track->chunkOffsets = new uint32[track->chunkCount]; if (!track->chunkOffsets) return -1; for (uint32 i = 0; i < track->chunkCount; i++) { // WORKAROUND/HACK: The offsets in Riven videos (ones inside the Mohawk archives themselves) // have offsets relative to the archive and not the video. This is quite nasty. We subtract // the initial offset of the stream to get the correct value inside of the stream. track->chunkOffsets[i] = _fd->readUint32BE() - _beginOffset; } return 0; } int QuickTimeParser::readWAVE(Atom atom) { if (_tracks.empty()) return 0; Track *track = _tracks.back(); if (atom.size > (1 << 30)) return -1; // We should only get here within an stsd atom if (track->sampleDescs.empty()) return -1; SampleDesc *sampleDesc = track->sampleDescs.back(); if (sampleDesc->getCodecTag() == MKTAG('Q', 'D', 'M', '2')) // Read extra data for QDM2 sampleDesc->_extraData = _fd->readStream(atom.size); else if (atom.size > 8) return readDefault(atom); else _fd->skip(atom.size); return 0; } enum { kMP4IODescTag = 2, kMP4ESDescTag = 3, kMP4DecConfigDescTag = 4, kMP4DecSpecificDescTag = 5 }; static int readMP4DescLength(SeekableReadStream *stream) { int length = 0; int count = 4; while (count--) { byte c = stream->readByte(); length = (length << 7) | (c & 0x7f); if (!(c & 0x80)) break; } return length; } static void readMP4Desc(SeekableReadStream *stream, byte &tag, int &length) { tag = stream->readByte(); length = readMP4DescLength(stream); } int QuickTimeParser::readESDS(Atom atom) { if (_tracks.empty()) return 0; Track *track = _tracks.back(); // We should only get here within an stsd atom if (track->sampleDescs.empty()) return -1; SampleDesc *sampleDesc = track->sampleDescs.back(); _fd->readUint32BE(); // version + flags byte tag; int length; readMP4Desc(_fd, tag, length); _fd->readUint16BE(); // id if (tag == kMP4ESDescTag) _fd->readByte(); // priority // Check if we've got the Config MPEG-4 header readMP4Desc(_fd, tag, length); if (tag != kMP4DecConfigDescTag) return 0; sampleDesc->_objectTypeMP4 = _fd->readByte(); _fd->readByte(); // stream type _fd->readUint16BE(); _fd->readByte(); // buffer size _fd->readUint32BE(); // max bitrate _fd->readUint32BE(); // avg bitrate // Check if we've got the Specific MPEG-4 header readMP4Desc(_fd, tag, length); if (tag != kMP4DecSpecificDescTag) return 0; sampleDesc->_extraData = _fd->readStream(length); debugC(0, kDebugLevelGVideo, "MPEG-4 object type = %02x", sampleDesc->_objectTypeMP4); return 0; } int QuickTimeParser::readSMI(Atom atom) { if (_tracks.empty()) return 0; Track *track = _tracks.back(); // We should only get here within an stsd atom if (track->sampleDescs.empty()) return -1; SampleDesc *sampleDesc = track->sampleDescs.back(); // This atom just contains SVQ3 extra data sampleDesc->_extraData = _fd->readStream(atom.size); return 0; } int QuickTimeParser::readCTYP(Atom atom) { uint32 ctype = _fd->readUint32BE(); switch (ctype) { case MKTAG('s', 't', 'n', 'a'): _qtvrType = QTVRType::OBJECT; break; case MKTAG('S', 'T', 'p', 'n'): case MKTAG('s', 't', 'p', 'n'): _qtvrType = QTVRType::PANORAMA; break; case MKTAG('q', 't', 'v', 'r'): _qtvrType = QTVRType::OTHER; warning("QuickTimeParser::readCTYP(): QTVR 2.0 files are not yet supported"); break; default: _qtvrType = QTVRType::OTHER; warning("QuickTimeParser::readCTYP(): Unknown QTVR Type ('%s')", tag2str(ctype)); break; } return 0; } int QuickTimeParser::readWLOC(Atom atom) { _winX = _fd->readUint16BE(); _winY = _fd->readUint16BE(); return 0; } static float readAppleFloatField(SeekableReadStream *stream) { int16 a = stream->readSint16BE(); uint16 b = stream->readUint16BE(); float value = (float)a + (float)b / 65536.0f; return value; } int QuickTimeParser::readNAVG(Atom atom) { _fd->readUint16BE(); // version _nav.columns = _fd->readUint16BE(); _nav.rows = _fd->readUint16BE(); _fd->readUint16BE(); // reserved _nav.loop_size = _fd->readUint16BE(); _nav.frame_duration = _fd->readUint16BE(); _nav.movie_type = (MovieType)_fd->readUint16BE(); _nav.loop_ticks = _fd->readUint16BE(); _nav.field_of_view = readAppleFloatField(_fd); _nav.startHPan = readAppleFloatField(_fd); _nav.endHPan = readAppleFloatField(_fd); _nav.endVPan = readAppleFloatField(_fd); _nav.startVPan = readAppleFloatField(_fd); _nav.initialHPan = readAppleFloatField(_fd); _nav.initialVPan = readAppleFloatField(_fd); _fd->readUint32BE(); // reserved2 return 0; } int QuickTimeParser::readGMIN(Atom atom) { Track *track = _tracks.back(); _fd->readUint32BE(); // version + flags track->graphicsMode = (GraphicsMode)_fd->readUint16BE(); _fd->readMultipleBE(track->opcolor); track->soundBalance = _fd->readUint16BE(); _fd->readUint16BE(); // reserved return 0; } int QuickTimeParser::readPINF(Atom atom) { Track *track = _tracks.back(); track->panoInfo.name = _fd->readPascalString(); _fd->seek((int64)atom.offset + 32); track->panoInfo.defNodeID = _fd->readUint32BE(); track->panoInfo.defZoom = readAppleFloatField(_fd); _fd->readUint32BE(); // reserved _fd->readSint16BE(); // padding int16 numEntries = _fd->readSint16BE(); track->panoInfo.nodes.resize(numEntries); for (int16 i = 0; i < numEntries; i++) { track->panoInfo.nodes[i].nodeID = _fd->readUint32BE(); track->panoInfo.nodes[i].timestamp = _fd->readUint32BE(); } return 0; } int QuickTimeParser::readDREF(Atom atom) { if (atom.size > 1) { Track *track = _tracks.back(); uint32 endPos = _fd->pos() + atom.size; _fd->readUint32BE(); // version + flags uint32 entries = _fd->readUint32BE(); for (uint32 i = 0; i < entries && _fd->pos() < endPos; i++) { uint32 size = _fd->readUint32BE(); uint32 next = _fd->pos() + size - 4; if (next > endPos) { warning("DREF chunk overflows atom bounds"); return 1; } uint32 type = _fd->readUint32BE(); _fd->readUint32BE(); // version + flags if (type == MKTAG('a', 'l', 'i', 's')) { if (size < 150) { _fd->seek(next, SEEK_SET); continue; } // Macintosh alias record _fd->seek(10, SEEK_CUR); uint8 volumeSize = MIN((uint8)27, _fd->readByte()); track->volume = _fd->readString('\0', volumeSize); _fd->seek(27 - volumeSize, SEEK_CUR); _fd->seek(12, SEEK_CUR); uint8 filenameSize = MIN((uint8)63, _fd->readByte()); track->filename = _fd->readString('\0', filenameSize); _fd->seek(63 - filenameSize, SEEK_CUR); _fd->seek(16, SEEK_CUR); debugC(5, kDebugLevelGVideo, "volume: %s, filename: %s", track->volume.c_str(), track->filename.c_str()); track->nlvlFrom = _fd->readSint16BE(); track->nlvlTo = _fd->readSint16BE(); _fd->seek(16, SEEK_CUR); debugC(5, kDebugLevelGVideo, "nlvlFrom: %d, nlvlTo: %d", track->nlvlFrom, track->nlvlTo); for (int16 subType = 0; subType != -1 && _fd->pos() < endPos;) { subType = _fd->readSint16BE(); uint16 subTypeSize = _fd->readUint16BE(); subTypeSize += subTypeSize & 1 ? 1 : 0; if (subType == 2) { // Absolute path track->path = _fd->readString('\0', subTypeSize); if (track->path.substr(0, volumeSize) == track->volume) { track->path = track->path.substr(volumeSize); } debugC(5, kDebugLevelGVideo, "path: %s", track->path.c_str()); } else if (subType == 0) { track->directory = _fd->readString('\0', subTypeSize); debugC(5, kDebugLevelGVideo, "directory: %s", track->directory.c_str()); } else { _fd->seek(subTypeSize, SEEK_CUR); } } } else { warning("Unknown DREF type %s", tag2str(type)); _fd->seek(next, SEEK_SET); } } _fd->seek(endPos, SEEK_SET); } return 0; } int QuickTimeParser::readPHDR(Atom atom) { PanoSampleHeader &pHdr = _panoTrack->panoSamples.back().hdr; pHdr.nodeID = _fd->readUint32BE(); pHdr.defHPan = readAppleFloatField(_fd); pHdr.defVPan = readAppleFloatField(_fd); pHdr.defZoom = readAppleFloatField(_fd); pHdr.minHPan = readAppleFloatField(_fd); pHdr.minVPan = readAppleFloatField(_fd); pHdr.minZoom = readAppleFloatField(_fd); pHdr.maxHPan = readAppleFloatField(_fd); pHdr.maxVPan = readAppleFloatField(_fd); pHdr.minHPan = readAppleFloatField(_fd); _fd->readSint64BE(); // reserved1 + reserved2 pHdr.nameStrOffset = _fd->readSint32BE(); pHdr.commentStrOffset = _fd->readSint32BE(); return 0; } int QuickTimeParser::readPHOT(Atom atom) { PanoHotSpotTable &pHotSpotTable = _panoTrack->panoSamples.back().hotSpotTable; _fd->readUint16BE(); // padding int16 numHotSpots = _fd->readSint16BE(); pHotSpotTable.hotSpots.resize(numHotSpots); for (int i = 0; i < numHotSpots; i++) { pHotSpotTable.hotSpots[i].id = _fd->readUint16BE(); _fd->readUint16BE(); // reserved pHotSpotTable.hotSpots[i].type = _fd->readUint32BE(); pHotSpotTable.hotSpots[i].typeData = _fd->readUint32BE(); pHotSpotTable.hotSpots[i].viewHPan = readAppleFloatField(_fd); pHotSpotTable.hotSpots[i].viewVPan = readAppleFloatField(_fd); pHotSpotTable.hotSpots[i].viewZoom = readAppleFloatField(_fd); pHotSpotTable.hotSpots[i].rect.top = _fd->readSint16BE(); pHotSpotTable.hotSpots[i].rect.left = _fd->readSint16BE(); pHotSpotTable.hotSpots[i].rect.right = _fd->readSint16BE(); pHotSpotTable.hotSpots[i].rect.bottom = _fd->readSint16BE(); pHotSpotTable.hotSpots[i].mouseOverCursorID = _fd->readSint32BE(); pHotSpotTable.hotSpots[i].mouseDownCursorID = _fd->readSint32BE(); pHotSpotTable.hotSpots[i].mouseUpCursorID = _fd->readSint32BE(); _fd->readSint32BE(); // reserved2 pHotSpotTable.hotSpots[i].nameStrOffset = _fd->readSint32BE(); pHotSpotTable.hotSpots[i].commentStrOffset = _fd->readSint32BE(); } return 0; } int QuickTimeParser::readSTRT(Atom atom) { PanoStringTable &pStrTable = _panoTrack->panoSamples.back().strTable; pStrTable.strings = _fd->readString(0, atom.size); return 0; } int QuickTimeParser::readPLNK(Atom atom) { PanoLinkTable &pLinkTable = _panoTrack->panoSamples.back().linkTable; _fd->readUint16BE(); // padding int16 numLinks = _fd->readSint16BE(); pLinkTable.links.resize(numLinks); for (int i = 0; i < numLinks; i++) { pLinkTable.links[i].id = _fd->readUint16BE(); _fd->readUint16BE(); // reserved _fd->readUint64BE(); // reserved2 + reserved3 pLinkTable.links[i].toNodeID = _fd->readUint32BE(); _fd->skip(12); // reserved4 pLinkTable.links[i].toHPan = readAppleFloatField(_fd); pLinkTable.links[i].toVPan = readAppleFloatField(_fd); pLinkTable.links[i].toZoom = readAppleFloatField(_fd); _fd->readUint64BE(); // reserved5 + reserved6 pLinkTable.links[i].nameStrOffset = _fd->readSint32BE(); pLinkTable.links[i].commentStrOffset = _fd->readSint32BE(); } return 0; } int QuickTimeParser::readPNAV(Atom atom) { PanoNavigationTable &pLinkTable = _panoTrack->panoSamples.back().navTable; _fd->readUint16BE(); // padding int16 numNavs = _fd->readSint16BE(); pLinkTable.navs.resize(numNavs); for (int i = 0; i < numNavs; i++) { pLinkTable.navs[i].id = _fd->readUint16BE(); _fd->readUint16BE(); // reserved _fd->readUint32BE(); // reserved2 pLinkTable.navs[i].navgHPan = readAppleFloatField(_fd); pLinkTable.navs[i].navgVPan = readAppleFloatField(_fd); pLinkTable.navs[i].navgZoom = readAppleFloatField(_fd); pLinkTable.navs[i].zoomRect.top = _fd->readSint16BE(); pLinkTable.navs[i].zoomRect.left = _fd->readSint16BE(); pLinkTable.navs[i].zoomRect.right = _fd->readSint16BE(); pLinkTable.navs[i].zoomRect.bottom = _fd->readSint16BE(); _fd->readSint32BE(); // reserved3 pLinkTable.navs[i].nameStrOffset = _fd->readSint32BE(); pLinkTable.navs[i].commentStrOffset = _fd->readSint32BE(); } return 0; } void QuickTimeParser::close() { for (uint32 i = 0; i < _tracks.size(); i++) delete _tracks[i]; _tracks.clear(); if (_disposeFileHandle == DisposeAfterUse::YES) delete _fd; _fd = nullptr; } void QuickTimeParser::flattenEditLists() { // This flattens the movie edit list, collapsing everything into a single edit. // This is necessary to work around sound popping in Obsidian on certain movies: // // For some reason, numerous movies have audio tracks with edit lists consisting // off numerous 0.5-second duration chunks, which is 22050 audio examples at the // 44100Hz media rate, but the edit media times are spaced out 22080 apart. // // The QuickTime File Format reference seems to suggest that this means the audio // would skip ahead 30 samples every half-second, which is what the playback code // currently does, and that causes audible popping in some movies (such as the // cube maze greeter vidbot when she says "Take take take, that's all you ever do!" // after repeatedly visiting her.) // // Other players seem to just play the audio track chunks consecutively without the // 30-sample skips, which produces the correct results, not sure why. // // // We also need to account for mixed silent and non-silent tracks. In Obsidian's // Japanese localization, the vidbot that you talk to at the end of the maze (asset // 4375) has a brief silent edit followed by the actual audio track. If we // collapse the audio track into the silent edit, then it causes the entire track // to be silent. for (Track *track : _tracks) { if (track->editList.size() >= 2) { Common::Array newEdits; for (const EditListEntry &curEdit : track->editList) { if (newEdits.size() == 0) { newEdits.push_back(curEdit); continue; } EditListEntry &prevEdit = newEdits.back(); bool prevIsSilent = (prevEdit.mediaTime == -1); bool currentIsSilent = (curEdit.mediaTime == -1); if (prevIsSilent != currentIsSilent) { newEdits.push_back(curEdit); continue; } else prevEdit.trackDuration += curEdit.trackDuration; } track->editList = Common::move(newEdits); } } } QuickTimeParser::SampleDesc::SampleDesc(Track *parentTrack, uint32 codecTag) { _parentTrack = parentTrack; _codecTag = codecTag; _extraData = nullptr; _objectTypeMP4 = 0; } QuickTimeParser::SampleDesc::~SampleDesc() { delete _extraData; } QuickTimeParser::Track::Track() { chunkCount = 0; chunkOffsets = nullptr; timeToSampleCount = 0; timeToSample = nullptr; sampleToChunkCount = 0; sampleToChunk = nullptr; sampleSize = 0; sampleCount = 0; sampleSizes = nullptr; keyframeCount = 0; keyframes = nullptr; timeScale = 0; width = 0; height = 0; codecType = CODEC_TYPE_MOV_OTHER; frameCount = 0; duration = 0; mediaDuration = 0; nlvlFrom = -1; nlvlTo = -1; graphicsMode = GraphicsMode::COPY; opcolor[0] = opcolor[1] = opcolor[2] = 0; soundBalance = 0; targetTrack = 0; } String QuickTimeParser::PanoStringTable::getString(int32 offset) const { offset -= 8; if (offset < 0) return String(); int32 str_start = offset + 1; int32 str_length = strings[offset]; return strings.substr(str_start, str_length); } QuickTimeParser::Track::~Track() { delete[] chunkOffsets; delete[] timeToSample; delete[] sampleToChunk; delete[] sampleSizes; delete[] keyframes; for (uint32 i = 0; i < sampleDescs.size(); i++) delete sampleDescs[i]; } } // End of namespace Video