/********** This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. (See .) This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA **********/ // "liveMedia" // Copyright (c) 1996-2016 Live Networks, Inc. All rights reserved. // A class that encapsulates MPEG-2 Transport Stream 'index files'/ // These index files are used to implement 'trick play' operations // (seek-by-time, fast forward, reverse play) on Transport Stream files. // // Implementation #include "MPEG2TransportStreamIndexFile.hh" #include "InputFile.hh" MPEG2TransportStreamIndexFile ::MPEG2TransportStreamIndexFile(UsageEnvironment& env, char const* indexFileName) : Medium(env), fFileName(strDup(indexFileName)), fFid(NULL), fMPEGVersion(0), fCurrentIndexRecordNum(0), fCachedPCR(0.0f), fCachedTSPacketNumber(0), fNumIndexRecords(0) { // Get the file size, to determine how many index records it contains: u_int64_t indexFileSize = GetFileSize(indexFileName, NULL); if (indexFileSize % INDEX_RECORD_SIZE != 0) { env << "Warning: Size of the index file \"" << indexFileName << "\" (" << (unsigned)indexFileSize << ") is not a multiple of the index record size (" << INDEX_RECORD_SIZE << ")\n"; } fNumIndexRecords = (unsigned long)(indexFileSize/INDEX_RECORD_SIZE); } MPEG2TransportStreamIndexFile* MPEG2TransportStreamIndexFile ::createNew(UsageEnvironment& env, char const* indexFileName) { if (indexFileName == NULL) return NULL; MPEG2TransportStreamIndexFile* indexFile = new MPEG2TransportStreamIndexFile(env, indexFileName); // Reject empty or non-existent index files: if (indexFile->getPlayingDuration() == 0.0f) { delete indexFile; indexFile = NULL; } return indexFile; } MPEG2TransportStreamIndexFile::~MPEG2TransportStreamIndexFile() { closeFid(); delete[] fFileName; } void MPEG2TransportStreamIndexFile ::lookupTSPacketNumFromNPT(float& npt, unsigned long& tsPacketNumber, unsigned long& indexRecordNumber) { if (npt <= 0.0 || fNumIndexRecords == 0) { // Fast-track a common case: npt = 0.0f; tsPacketNumber = indexRecordNumber = 0; return; } // If "npt" is the same as the one that we last looked up, return its cached result: if (npt == fCachedPCR) { tsPacketNumber = fCachedTSPacketNumber; indexRecordNumber = fCachedIndexRecordNumber; return; } // Search for the pair of neighboring index records whose PCR values span "npt". // Use the 'regula-falsi' method. Boolean success = False; unsigned long ixFound = 0; do { unsigned long ixLeft = 0, ixRight = fNumIndexRecords-1; float pcrLeft = 0.0f, pcrRight; if (!readIndexRecord(ixRight)) break; pcrRight = pcrFromBuf(); if (npt > pcrRight) npt = pcrRight; // handle "npt" too large by seeking to the last frame of the file while (ixRight-ixLeft > 1 && pcrLeft < npt && npt <= pcrRight) { unsigned long ixNew = ixLeft + (unsigned long)(((npt-pcrLeft)/(pcrRight-pcrLeft))*(ixRight-ixLeft)); if (ixNew == ixLeft || ixNew == ixRight) { // use bisection instead: ixNew = (ixLeft+ixRight)/2; } if (!readIndexRecord(ixNew)) break; float pcrNew = pcrFromBuf(); if (pcrNew < npt) { pcrLeft = pcrNew; ixLeft = ixNew; } else { pcrRight = pcrNew; ixRight = ixNew; } } if (ixRight-ixLeft > 1 || npt <= pcrLeft || npt > pcrRight) break; // bad PCR values in index file? ixFound = ixRight; // "Rewind' until we reach the start of a Video Sequence or GOP header: success = rewindToCleanPoint(ixFound); } while (0); if (success && readIndexRecord(ixFound)) { // Return (and cache) information from record "ixFound": npt = fCachedPCR = pcrFromBuf(); tsPacketNumber = fCachedTSPacketNumber = tsPacketNumFromBuf(); indexRecordNumber = fCachedIndexRecordNumber = ixFound; } else { // An error occurred: Return the default values, for npt == 0: npt = 0.0f; tsPacketNumber = indexRecordNumber = 0; } closeFid(); } void MPEG2TransportStreamIndexFile ::lookupPCRFromTSPacketNum(unsigned long& tsPacketNumber, Boolean reverseToPreviousCleanPoint, float& pcr, unsigned long& indexRecordNumber) { if (tsPacketNumber == 0 || fNumIndexRecords == 0) { // Fast-track a common case: pcr = 0.0f; indexRecordNumber = 0; return; } // If "tsPacketNumber" is the same as the one that we last looked up, return its cached result: if (tsPacketNumber == fCachedTSPacketNumber) { pcr = fCachedPCR; indexRecordNumber = fCachedIndexRecordNumber; return; } // Search for the pair of neighboring index records whose TS packet #s span "tsPacketNumber". // Use the 'regula-falsi' method. Boolean success = False; unsigned long ixFound = 0; do { unsigned long ixLeft = 0, ixRight = fNumIndexRecords-1; unsigned long tsLeft = 0, tsRight; if (!readIndexRecord(ixRight)) break; tsRight = tsPacketNumFromBuf(); if (tsPacketNumber > tsRight) tsPacketNumber = tsRight; // handle "tsPacketNumber" too large by seeking to the last frame of the file while (ixRight-ixLeft > 1 && tsLeft < tsPacketNumber && tsPacketNumber <= tsRight) { unsigned long ixNew = ixLeft + (unsigned long)(((tsPacketNumber-tsLeft)/(tsRight-tsLeft))*(ixRight-ixLeft)); if (ixNew == ixLeft || ixNew == ixRight) { // Use bisection instead: ixNew = (ixLeft+ixRight)/2; } if (!readIndexRecord(ixNew)) break; unsigned long tsNew = tsPacketNumFromBuf(); if (tsNew < tsPacketNumber) { tsLeft = tsNew; ixLeft = ixNew; } else { tsRight = tsNew; ixRight = ixNew; } } if (ixRight-ixLeft > 1 || tsPacketNumber <= tsLeft || tsPacketNumber > tsRight) break; // bad PCR values in index file? ixFound = ixRight; if (reverseToPreviousCleanPoint) { // "Rewind' until we reach the start of a Video Sequence or GOP header: success = rewindToCleanPoint(ixFound); } else { success = True; } } while (0); if (success && readIndexRecord(ixFound)) { // Return (and cache) information from record "ixFound": pcr = fCachedPCR = pcrFromBuf(); fCachedTSPacketNumber = tsPacketNumFromBuf(); if (reverseToPreviousCleanPoint) tsPacketNumber = fCachedTSPacketNumber; indexRecordNumber = fCachedIndexRecordNumber = ixFound; } else { // An error occurred: Return the default values, for tsPacketNumber == 0: pcr = 0.0f; indexRecordNumber = 0; } closeFid(); } Boolean MPEG2TransportStreamIndexFile ::readIndexRecordValues(unsigned long indexRecordNum, unsigned long& transportPacketNum, u_int8_t& offset, u_int8_t& size, float& pcr, u_int8_t& recordType) { if (!readIndexRecord(indexRecordNum)) return False; transportPacketNum = tsPacketNumFromBuf(); offset = offsetFromBuf(); size = sizeFromBuf(); pcr = pcrFromBuf(); recordType = recordTypeFromBuf(); return True; } float MPEG2TransportStreamIndexFile::getPlayingDuration() { if (fNumIndexRecords == 0 || !readOneIndexRecord(fNumIndexRecords-1)) return 0.0f; return pcrFromBuf(); } int MPEG2TransportStreamIndexFile::mpegVersion() { if (fMPEGVersion != 0) return fMPEGVersion; // we already know it // Read the first index record, and figure out the MPEG version from its type: if (!readOneIndexRecord(0)) return 0; // unknown; perhaps the indecx file is empty? setMPEGVersionFromRecordType(recordTypeFromBuf()); return fMPEGVersion; } Boolean MPEG2TransportStreamIndexFile::openFid() { if (fFid == NULL && fFileName != NULL) { if ((fFid = OpenInputFile(envir(), fFileName)) != NULL) { fCurrentIndexRecordNum = 0; } } return fFid != NULL; } Boolean MPEG2TransportStreamIndexFile::seekToIndexRecord(unsigned long indexRecordNumber) { if (!openFid()) return False; if (indexRecordNumber == fCurrentIndexRecordNum) return True; // we're already there if (SeekFile64(fFid, (int64_t)(indexRecordNumber*INDEX_RECORD_SIZE), SEEK_SET) != 0) return False; fCurrentIndexRecordNum = indexRecordNumber; return True; } Boolean MPEG2TransportStreamIndexFile::readIndexRecord(unsigned long indexRecordNum) { do { if (!seekToIndexRecord(indexRecordNum)) break; if (fread(fBuf, INDEX_RECORD_SIZE, 1, fFid) != 1) break; ++fCurrentIndexRecordNum; return True; } while (0); return False; // an error occurred } Boolean MPEG2TransportStreamIndexFile::readOneIndexRecord(unsigned long indexRecordNum) { Boolean result = readIndexRecord(indexRecordNum); closeFid(); return result; } void MPEG2TransportStreamIndexFile::closeFid() { if (fFid != NULL) { CloseInputFile(fFid); fFid = NULL; } } float MPEG2TransportStreamIndexFile::pcrFromBuf() { unsigned pcr_int = (fBuf[5]<<16) | (fBuf[4]<<8) | fBuf[3]; u_int8_t pcr_frac = fBuf[6]; return pcr_int + pcr_frac/256.0f; } unsigned long MPEG2TransportStreamIndexFile::tsPacketNumFromBuf() { return (fBuf[10]<<24) | (fBuf[9]<<16) | (fBuf[8]<<8) | fBuf[7]; } void MPEG2TransportStreamIndexFile::setMPEGVersionFromRecordType(u_int8_t recordType) { if (fMPEGVersion != 0) return; // we already know it u_int8_t const recordTypeWithoutStartBit = recordType&~0x80; if (recordTypeWithoutStartBit >= 1 && recordTypeWithoutStartBit <= 4) fMPEGVersion = 2; else if (recordTypeWithoutStartBit >= 5 && recordTypeWithoutStartBit <= 10) fMPEGVersion = 5; // represents H.264 else if (recordTypeWithoutStartBit >= 11 && recordTypeWithoutStartBit <= 16) fMPEGVersion = 6; // represents H.265 } Boolean MPEG2TransportStreamIndexFile::rewindToCleanPoint(unsigned long&ixFound) { Boolean success = False; // until we learn otherwise while (ixFound > 0) { if (!readIndexRecord(ixFound)) break; u_int8_t recordType = recordTypeFromBuf(); setMPEGVersionFromRecordType(recordType); // A 'clean point' is the start of a 'frame' from which a decoder can cleanly resume // handling the stream. For H.264, this is a SPS. For H.265, this is a VPS. // For MPEG-2, this is a Video Sequence Header, or a GOP. if ((recordType&0x80) != 0) { // This is the start of a 'frame' recordType &=~ 0x80; // remove the 'start of frame' bit if (fMPEGVersion == 5) { // H.264 if (recordType == 5/*SPS*/) { success = True; break; } } else if (fMPEGVersion == 6) { // H.265 if (recordType == 11/*VPS*/) { success = True; break; } } else { // MPEG-1, 2, or 4 if (recordType == 1/*VSH*/) { success = True; break; } else if (recordType == 2/*GOP*/) { // Hack: If the preceding record is for a Video Sequence Header, then use it instead: unsigned long newIxFound = ixFound; while (--newIxFound > 0) { if (!readIndexRecord(newIxFound)) break; recordType = recordTypeFromBuf(); if ((recordType&0x7F) != 1) break; // not a Video Sequence Header if ((recordType&0x80) != 0) { // this is the start of the VSH; use it ixFound = newIxFound; break; } } } success = True; break; } } // Keep checking, from the previous record: --ixFound; } if (ixFound == 0) success = True; // use record 0 anyway return success; }