/********** 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 3 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-2020 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; }