350 lines
12 KiB
C++
350 lines
12 KiB
C++
|
/**********
|
||
|
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 <http://www.gnu.org/copyleft/lesser.html>.)
|
||
|
|
||
|
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;
|
||
|
}
|