From 74cc4aec8aa80da2541857e3120b31a566ccdff3 Mon Sep 17 00:00:00 2001 From: Matthew Hoops Date: Sun, 25 Aug 2013 22:23:33 -0400 Subject: VIDEO: Add support for seeking in AVI videos with an index Rewinding will work in any AVI video --- video/avi_decoder.cpp | 325 ++++++++++++++++++++++++++++++++++++++++++++------ video/avi_decoder.h | 20 +++- 2 files changed, 309 insertions(+), 36 deletions(-) (limited to 'video') diff --git a/video/avi_decoder.cpp b/video/avi_decoder.cpp index 34bc8c4a7a..5e4b91d1bd 100644 --- a/video/avi_decoder.cpp +++ b/video/avi_decoder.cpp @@ -80,6 +80,13 @@ namespace Video { #define ID_DUCK MKTAG('D','U','C','K') #define ID_MPG2 MKTAG('m','p','g','2') +// Stream Types +enum { + kStreamTypePaletteChange = MKTAG16('p', 'c'), + kStreamTypeRawVideo = MKTAG16('d', 'b'), + kStreamTypeAudio = MKTAG16('w', 'b') +}; + AVIDecoder::AVIDecoder(Audio::Mixer::SoundType soundType) : _frameRateOverride(0), _soundType(soundType) { initCommon(); @@ -102,6 +109,12 @@ void AVIDecoder::initCommon() { memset(&_header, 0, sizeof(_header)); } +bool AVIDecoder::isSeekable() const { + // Only videos with an index can seek + // Anyone else who wants to seek is crazy. + return isVideoLoaded() && !_indexEntries.empty(); +} + bool AVIDecoder::parseNextChunk() { uint32 tag = _fileStream->readUint32BE(); uint32 size = _fileStream->readUint32LE(); @@ -146,10 +159,10 @@ bool AVIDecoder::parseNextChunk() { OldIndex indexEntry; indexEntry.id = _fileStream->readUint32BE(); indexEntry.flags = _fileStream->readUint32LE(); - indexEntry.offset = _fileStream->readUint32LE(); + indexEntry.offset = _fileStream->readUint32LE() + _movieListStart - 4; // Adjust to absolute indexEntry.size = _fileStream->readUint32LE(); _indexEntries.push_back(indexEntry); - debug(0, "Index %d == Tag \'%s\', Offset = %d, Size = %d", i, tag2str(indexEntry.id), indexEntry.offset, indexEntry.size); + debug(0, "Index %d == Tag \'%s\', Offset = %d, Size = %d (Flags = %d)", i, tag2str(indexEntry.id), indexEntry.offset, indexEntry.size, indexEntry.flags); } break; default: @@ -250,21 +263,22 @@ void AVIDecoder::handleStreamHeader(uint32 size) { if (sHeader.streamHandler == 0) sHeader.streamHandler = bmInfo.compression; - AVIVideoTrack *track = new AVIVideoTrack(_header.totalFrames, sHeader, bmInfo); + byte *initialPalette = 0; if (bmInfo.bitCount == 8) { - byte *palette = const_cast(track->getPalette()); + initialPalette = new byte[256 * 3]; + memset(initialPalette, 0, 256 * 3); + + byte *palette = initialPalette; for (uint32 i = 0; i < bmInfo.clrUsed; i++) { palette[i * 3 + 2] = _fileStream->readByte(); palette[i * 3 + 1] = _fileStream->readByte(); palette[i * 3] = _fileStream->readByte(); _fileStream->readByte(); } - - track->markPaletteDirty(); } - addTrack(track); + addTrack(new AVIVideoTrack(_header.totalFrames, sHeader, bmInfo, initialPalette)); } else if (sHeader.streamType == ID_AUDS) { PCMWaveFormat wvInfo; wvInfo.tag = _fileStream->readUint16LE(); @@ -379,7 +393,7 @@ void AVIDecoder::readNextPacket() { } if (track->getTrackType() == Track::kTrackTypeAudio) { - if (getStreamType(nextTag) != MKTAG16('w', 'b')) + if (getStreamType(nextTag) != kStreamTypeAudio) error("Invalid audio track tag '%s'", tag2str(nextTag)); assert(chunk); @@ -387,29 +401,10 @@ void AVIDecoder::readNextPacket() { } else { AVIVideoTrack *videoTrack = (AVIVideoTrack *)track; - if (getStreamType(nextTag) == MKTAG16('p', 'c')) { + if (getStreamType(nextTag) == kStreamTypePaletteChange) { // Palette Change - assert(chunk); - byte firstEntry = chunk->readByte(); - uint16 numEntries = chunk->readByte(); - chunk->readUint16LE(); // Reserved - - // 0 entries means all colors are going to be changed - if (numEntries == 0) - numEntries = 256; - - byte *palette = const_cast(videoTrack->getPalette()); - - for (uint16 i = firstEntry; i < numEntries + firstEntry; i++) { - palette[i * 3] = chunk->readByte(); - palette[i * 3 + 1] = chunk->readByte(); - palette[i * 3 + 2] = chunk->readByte(); - chunk->readByte(); // Flags that don't serve us any purpose - } - - delete chunk; - videoTrack->markPaletteDirty(); - } else if (getStreamType(nextTag) == MKTAG16('d', 'b')) { + videoTrack->loadPaletteFromChunk(chunk); + } else if (getStreamType(nextTag) == kStreamTypeRawVideo) { // TODO: Check if this really is uncompressed. Many videos // falsely put compressed data in here. error("Uncompressed AVI frame found"); @@ -420,6 +415,201 @@ void AVIDecoder::readNextPacket() { } } +bool AVIDecoder::rewind() { + if (!VideoDecoder::rewind()) + return false; + + _fileStream->seek(_movieListStart); + return true; +} + +bool AVIDecoder::seekIntern(const Audio::Timestamp &time) { + // Can't seek beyond the end + if (time > getDuration()) + return false; + + // Track down our video track (optionally audio too). + // We only support seeking with one track right now. + AVIVideoTrack *videoTrack = 0; + AVIAudioTrack *audioTrack = 0; + int videoIndex = -1; + int audioIndex = -1; + uint trackID = 0; + + for (TrackListIterator it = getTrackListBegin(); it != getTrackListEnd(); it++, trackID++) { + if ((*it)->getTrackType() == Track::kTrackTypeVideo) { + if (videoTrack) { + // Already have one + // -> Not supported + return false; + } + + videoTrack = (AVIVideoTrack *)*it; + videoIndex = trackID; + } else if ((*it)->getTrackType() == Track::kTrackTypeAudio) { + if (audioTrack) { + // Already have one + // -> Not supported + return false; + } + + audioTrack = (AVIAudioTrack *)*it; + audioIndex = trackID; + } + } + + // Need a video track to go forwards + // If there isn't a video track, why would anyone be using AVI then? + if (!videoTrack) + return false; + + // If we seek directly to the end, just mark the tracks as over + if (time == getDuration()) { + videoTrack->setCurFrame(videoTrack->getFrameCount() - 1); + + if (audioTrack) + audioTrack->resetStream(); + + return true; + } + + // Get the frame we should be on at this time + uint frame = videoTrack->getFrameAtTime(time); + + // Reset any palette, if necessary + videoTrack->useInitialPalette(); + + int lastKeyFrame = -1; + int frameIndex = -1; + int lastRecord = -1; + uint curFrame = 0; + + // Go through and figure out where we should be + // If there's a palette, we need to find the palette too + for (uint32 i = 0; i < _indexEntries.size(); i++) { + const OldIndex &index = _indexEntries[i]; + + if (index.id == ID_REC) { + // Keep track of any records we find + lastRecord = i; + } else { + if (getStreamIndex(index.id) != videoIndex) + continue; + + uint16 streamType = getStreamType(index.id); + + if (streamType == kStreamTypePaletteChange) { + // We need to handle any palette change we see since there's no + // flag to tell if this is a "key" palette. + // Decode the palette + _fileStream->seek(_indexEntries[i].offset + 8); + Common::SeekableReadStream *chunk = 0; + + if (_indexEntries[i].size != 0) + chunk = _fileStream->readStream(_indexEntries[i].size); + + videoTrack->loadPaletteFromChunk(chunk); + } else { + // Check to see if this is a keyframe + // The first frame has to be a keyframe + if ((_indexEntries[i].flags & AVIIF_INDEX) || curFrame == 0) + lastKeyFrame = i; + + // Did we find the target frame? + if (frame == curFrame) { + frameIndex = i; + break; + } + + curFrame++; + } + } + } + + if (frameIndex < 0) // This shouldn't happen. + return false; + + if (audioTrack) { + // We need to find where the start of audio should be. + // Which is exactly 'initialFrames' audio chunks back from where + // our found frame is. + + // Recreate the audio stream + audioTrack->resetStream(); + + uint framesNeeded = _header.initialFrames; + uint startAudioChunk = 0; + int startAudioSearch = (lastRecord < 0) ? (frameIndex - 1) : (lastRecord - 1); + + for (int i = startAudioSearch; i >= 0; i--) { + if (getStreamIndex(_indexEntries[i].id) != audioIndex) + continue; + + assert(getStreamType(_indexEntries[i].id) == kStreamTypeAudio); + + framesNeeded--; + + if (framesNeeded == 0) { + startAudioChunk = i; + break; + } + } + + // Now go forward and queue them all + for (int i = startAudioChunk; i <= startAudioSearch; i++) { + if (_indexEntries[i].id == ID_REC) + continue; + + if (getStreamIndex(_indexEntries[i].id) != audioIndex) + continue; + + assert(getStreamType(_indexEntries[i].id) == kStreamTypeAudio); + + _fileStream->seek(_indexEntries[i].offset + 8); + Common::SeekableReadStream *chunk = _fileStream->readStream(_indexEntries[i].size); + audioTrack->queueSound(chunk); + } + + // Skip any audio to bring us to the right time + audioTrack->skipAudio(time, videoTrack->getFrameTime(frame)); + } + + // Decode from keyFrame to curFrame - 1 + for (int i = lastKeyFrame; i < frameIndex; i++) { + if (_indexEntries[i].id == ID_REC) + continue; + + if (getStreamIndex(_indexEntries[i].id) != videoIndex) + continue; + + uint16 streamType = getStreamType(_indexEntries[i].id); + + // Ignore palettes, they were already handled + if (streamType == kStreamTypePaletteChange) + continue; + + // Frame, hopefully + _fileStream->seek(_indexEntries[i].offset + 8); + Common::SeekableReadStream *chunk = 0; + + if (_indexEntries[i].size != 0) + chunk = _fileStream->readStream(_indexEntries[i].size); + + videoTrack->decodeFrame(chunk); + } + + // Seek to the right spot + // To the beginning of the last record, or frame if that doesn't exist + if (lastRecord >= 0) + _fileStream->seek(_indexEntries[lastRecord].offset); + else + _fileStream->seek(_indexEntries[frameIndex].offset); + + videoTrack->setCurFrame((int)frame - 1); + + return true; +} + byte AVIDecoder::getStreamIndex(uint32 tag) const { char string[3]; WRITE_BE_UINT16(string, tag >> 16); @@ -427,17 +617,18 @@ byte AVIDecoder::getStreamIndex(uint32 tag) const { return strtol(string, 0, 16); } -AVIDecoder::AVIVideoTrack::AVIVideoTrack(int frameCount, const AVIStreamHeader &streamHeader, const BitmapInfoHeader &bitmapInfoHeader) - : _frameCount(frameCount), _vidsHeader(streamHeader), _bmInfo(bitmapInfoHeader) { - memset(_palette, 0, sizeof(_palette)); +AVIDecoder::AVIVideoTrack::AVIVideoTrack(int frameCount, const AVIStreamHeader &streamHeader, const BitmapInfoHeader &bitmapInfoHeader, byte *initialPalette) + : _frameCount(frameCount), _vidsHeader(streamHeader), _bmInfo(bitmapInfoHeader), _initialPalette(initialPalette) { _videoCodec = createCodec(); - _dirtyPalette = false; _lastFrame = 0; _curFrame = -1; + + useInitialPalette(); } AVIDecoder::AVIVideoTrack::~AVIVideoTrack() { delete _videoCodec; + delete[] _initialPalette; } void AVIDecoder::AVIVideoTrack::decodeFrame(Common::SeekableReadStream *stream) { @@ -460,6 +651,47 @@ Graphics::PixelFormat AVIDecoder::AVIVideoTrack::getPixelFormat() const { return Graphics::PixelFormat(); } +void AVIDecoder::AVIVideoTrack::loadPaletteFromChunk(Common::SeekableReadStream *chunk) { + assert(chunk); + byte firstEntry = chunk->readByte(); + uint16 numEntries = chunk->readByte(); + chunk->readUint16LE(); // Reserved + + // 0 entries means all colors are going to be changed + if (numEntries == 0) + numEntries = 256; + + for (uint16 i = firstEntry; i < numEntries + firstEntry; i++) { + _palette[i * 3] = chunk->readByte(); + _palette[i * 3 + 1] = chunk->readByte(); + _palette[i * 3 + 2] = chunk->readByte(); + chunk->readByte(); // Flags that don't serve us any purpose + } + + delete chunk; + _dirtyPalette = true; +} + +void AVIDecoder::AVIVideoTrack::useInitialPalette() { + _dirtyPalette = false; + + if (_initialPalette) { + memcpy(_palette, _initialPalette, sizeof(_palette)); + _dirtyPalette = true; + } +} + +bool AVIDecoder::AVIVideoTrack::rewind() { + _curFrame = -1; + + useInitialPalette(); + + delete _videoCodec; + _videoCodec = createCodec(); + _lastFrame = 0; + return true; +} + Codec *AVIDecoder::AVIVideoTrack::createCodec() { switch (_vidsHeader.streamHandler) { case ID_CRAM: @@ -521,6 +753,31 @@ void AVIDecoder::AVIAudioTrack::queueSound(Common::SeekableReadStream *stream) { } } +void AVIDecoder::AVIAudioTrack::skipAudio(const Audio::Timestamp &time, const Audio::Timestamp &frameTime) { + Audio::Timestamp timeDiff = time.convertToFramerate(_wvInfo.samplesPerSec) - frameTime.convertToFramerate(_wvInfo.samplesPerSec); + int skipFrames = timeDiff.totalNumberOfFrames(); + + if (skipFrames <= 0) + return; + + if (_audStream->isStereo()) + skipFrames *= 2; + + int16 *tempBuffer = new int16[skipFrames]; + _audStream->readBuffer(tempBuffer, skipFrames); + delete[] tempBuffer; +} + +void AVIDecoder::AVIAudioTrack::resetStream() { + delete _audStream; + _audStream = createAudioStream(); +} + +bool AVIDecoder::AVIAudioTrack::rewind() { + resetStream(); + return true; +} + Audio::AudioStream *AVIDecoder::AVIAudioTrack::getAudioStream() const { return _audStream; } diff --git a/video/avi_decoder.h b/video/avi_decoder.h index 5d52c7c797..fffcbfbe16 100644 --- a/video/avi_decoder.h +++ b/video/avi_decoder.h @@ -66,8 +66,13 @@ public: uint16 getWidth() const { return _header.width; } uint16 getHeight() const { return _header.height; } + bool rewind(); + bool isRewindable() const { return true; } + bool isSeekable() const; + protected: void readNextPacket(); + bool seekIntern(const Audio::Timestamp &time); private: struct BitmapInfoHeader { @@ -157,7 +162,7 @@ private: class AVIVideoTrack : public FixedRateVideoTrack { public: - AVIVideoTrack(int frameCount, const AVIStreamHeader &streamHeader, const BitmapInfoHeader &bitmapInfoHeader); + AVIVideoTrack(int frameCount, const AVIStreamHeader &streamHeader, const BitmapInfoHeader &bitmapInfoHeader, byte *initialPalette = 0); ~AVIVideoTrack(); void decodeFrame(Common::SeekableReadStream *stream); @@ -170,7 +175,12 @@ private: const Graphics::Surface *decodeNextFrame() { return _lastFrame; } const byte *getPalette() const { _dirtyPalette = false; return _palette; } bool hasDirtyPalette() const { return _dirtyPalette; } - void markPaletteDirty() { _dirtyPalette = true; } + void setCurFrame(int frame) { _curFrame = frame; } + void loadPaletteFromChunk(Common::SeekableReadStream *chunk); + void useInitialPalette(); + + bool isRewindable() const { return true; } + bool rewind(); protected: Common::Rational getFrameRate() const { return Common::Rational(_vidsHeader.rate, _vidsHeader.scale); } @@ -179,6 +189,7 @@ private: AVIStreamHeader _vidsHeader; BitmapInfoHeader _bmInfo; byte _palette[3 * 256]; + byte *_initialPalette; mutable bool _dirtyPalette; int _frameCount, _curFrame; @@ -194,6 +205,11 @@ private: void queueSound(Common::SeekableReadStream *stream); Audio::Mixer::SoundType getSoundType() const { return _soundType; } + void skipAudio(const Audio::Timestamp &time, const Audio::Timestamp &frameTime); + void resetStream(); + + bool isRewindable() const { return true; } + bool rewind(); protected: Audio::AudioStream *getAudioStream() const; -- cgit v1.2.3