1 | /********** |
2 | This library is free software; you can redistribute it and/or modify it under |
3 | the terms of the GNU Lesser General Public License as published by the |
4 | Free Software Foundation; either version 3 of the License, or (at your |
5 | option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.) |
6 | |
7 | This library is distributed in the hope that it will be useful, but WITHOUT |
8 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
9 | FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for |
10 | more details. |
11 | |
12 | You should have received a copy of the GNU Lesser General Public License |
13 | along with this library; if not, write to the Free Software Foundation, Inc., |
14 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
15 | **********/ |
16 | // "liveMedia" |
17 | // Copyright (c) 1996-2020 Live Networks, Inc. All rights reserved. |
18 | // A class that encapsulates MPEG-2 Transport Stream 'index files'/ |
19 | // These index files are used to implement 'trick play' operations |
20 | // (seek-by-time, fast forward, reverse play) on Transport Stream files. |
21 | // |
22 | // Implementation |
23 | |
24 | #include "MPEG2TransportStreamIndexFile.hh" |
25 | #include "InputFile.hh" |
26 | |
27 | MPEG2TransportStreamIndexFile |
28 | ::MPEG2TransportStreamIndexFile(UsageEnvironment& env, char const* indexFileName) |
29 | : Medium(env), |
30 | fFileName(strDup(indexFileName)), fFid(NULL), fMPEGVersion(0), fCurrentIndexRecordNum(0), |
31 | fCachedPCR(0.0f), fCachedTSPacketNumber(0), fNumIndexRecords(0) { |
32 | // Get the file size, to determine how many index records it contains: |
33 | u_int64_t indexFileSize = GetFileSize(indexFileName, NULL); |
34 | if (indexFileSize % INDEX_RECORD_SIZE != 0) { |
35 | env << "Warning: Size of the index file \"" << indexFileName |
36 | << "\" (" << (unsigned)indexFileSize |
37 | << ") is not a multiple of the index record size (" |
38 | << INDEX_RECORD_SIZE << ")\n" ; |
39 | } |
40 | fNumIndexRecords = (unsigned long)(indexFileSize/INDEX_RECORD_SIZE); |
41 | } |
42 | |
43 | MPEG2TransportStreamIndexFile* MPEG2TransportStreamIndexFile |
44 | ::createNew(UsageEnvironment& env, char const* indexFileName) { |
45 | if (indexFileName == NULL) return NULL; |
46 | MPEG2TransportStreamIndexFile* indexFile |
47 | = new MPEG2TransportStreamIndexFile(env, indexFileName); |
48 | |
49 | // Reject empty or non-existent index files: |
50 | if (indexFile->getPlayingDuration() == 0.0f) { |
51 | delete indexFile; |
52 | indexFile = NULL; |
53 | } |
54 | |
55 | return indexFile; |
56 | } |
57 | |
58 | MPEG2TransportStreamIndexFile::~MPEG2TransportStreamIndexFile() { |
59 | closeFid(); |
60 | delete[] fFileName; |
61 | } |
62 | |
63 | void MPEG2TransportStreamIndexFile |
64 | ::lookupTSPacketNumFromNPT(float& npt, unsigned long& tsPacketNumber, |
65 | unsigned long& indexRecordNumber) { |
66 | if (npt <= 0.0 || fNumIndexRecords == 0) { // Fast-track a common case: |
67 | npt = 0.0f; |
68 | tsPacketNumber = indexRecordNumber = 0; |
69 | return; |
70 | } |
71 | |
72 | // If "npt" is the same as the one that we last looked up, return its cached result: |
73 | if (npt == fCachedPCR) { |
74 | tsPacketNumber = fCachedTSPacketNumber; |
75 | indexRecordNumber = fCachedIndexRecordNumber; |
76 | return; |
77 | } |
78 | |
79 | // Search for the pair of neighboring index records whose PCR values span "npt". |
80 | // Use the 'regula-falsi' method. |
81 | Boolean success = False; |
82 | unsigned long ixFound = 0; |
83 | do { |
84 | unsigned long ixLeft = 0, ixRight = fNumIndexRecords-1; |
85 | float pcrLeft = 0.0f, pcrRight; |
86 | if (!readIndexRecord(ixRight)) break; |
87 | pcrRight = pcrFromBuf(); |
88 | if (npt > pcrRight) npt = pcrRight; |
89 | // handle "npt" too large by seeking to the last frame of the file |
90 | |
91 | while (ixRight-ixLeft > 1 && pcrLeft < npt && npt <= pcrRight) { |
92 | unsigned long ixNew = ixLeft |
93 | + (unsigned long)(((npt-pcrLeft)/(pcrRight-pcrLeft))*(ixRight-ixLeft)); |
94 | if (ixNew == ixLeft || ixNew == ixRight) { |
95 | // use bisection instead: |
96 | ixNew = (ixLeft+ixRight)/2; |
97 | } |
98 | if (!readIndexRecord(ixNew)) break; |
99 | float pcrNew = pcrFromBuf(); |
100 | if (pcrNew < npt) { |
101 | pcrLeft = pcrNew; |
102 | ixLeft = ixNew; |
103 | } else { |
104 | pcrRight = pcrNew; |
105 | ixRight = ixNew; |
106 | } |
107 | } |
108 | if (ixRight-ixLeft > 1 || npt <= pcrLeft || npt > pcrRight) break; // bad PCR values in index file? |
109 | |
110 | ixFound = ixRight; |
111 | // "Rewind' until we reach the start of a Video Sequence or GOP header: |
112 | success = rewindToCleanPoint(ixFound); |
113 | } while (0); |
114 | |
115 | if (success && readIndexRecord(ixFound)) { |
116 | // Return (and cache) information from record "ixFound": |
117 | npt = fCachedPCR = pcrFromBuf(); |
118 | tsPacketNumber = fCachedTSPacketNumber = tsPacketNumFromBuf(); |
119 | indexRecordNumber = fCachedIndexRecordNumber = ixFound; |
120 | } else { |
121 | // An error occurred: Return the default values, for npt == 0: |
122 | npt = 0.0f; |
123 | tsPacketNumber = indexRecordNumber = 0; |
124 | } |
125 | closeFid(); |
126 | } |
127 | |
128 | void MPEG2TransportStreamIndexFile |
129 | ::lookupPCRFromTSPacketNum(unsigned long& tsPacketNumber, Boolean reverseToPreviousCleanPoint, |
130 | float& pcr, unsigned long& indexRecordNumber) { |
131 | if (tsPacketNumber == 0 || fNumIndexRecords == 0) { // Fast-track a common case: |
132 | pcr = 0.0f; |
133 | indexRecordNumber = 0; |
134 | return; |
135 | } |
136 | |
137 | // If "tsPacketNumber" is the same as the one that we last looked up, return its cached result: |
138 | if (tsPacketNumber == fCachedTSPacketNumber) { |
139 | pcr = fCachedPCR; |
140 | indexRecordNumber = fCachedIndexRecordNumber; |
141 | return; |
142 | } |
143 | |
144 | // Search for the pair of neighboring index records whose TS packet #s span "tsPacketNumber". |
145 | // Use the 'regula-falsi' method. |
146 | Boolean success = False; |
147 | unsigned long ixFound = 0; |
148 | do { |
149 | unsigned long ixLeft = 0, ixRight = fNumIndexRecords-1; |
150 | unsigned long tsLeft = 0, tsRight; |
151 | if (!readIndexRecord(ixRight)) break; |
152 | tsRight = tsPacketNumFromBuf(); |
153 | if (tsPacketNumber > tsRight) tsPacketNumber = tsRight; |
154 | // handle "tsPacketNumber" too large by seeking to the last frame of the file |
155 | |
156 | while (ixRight-ixLeft > 1 && tsLeft < tsPacketNumber && tsPacketNumber <= tsRight) { |
157 | unsigned long ixNew = ixLeft |
158 | + (unsigned long)(((tsPacketNumber-tsLeft)/(tsRight-tsLeft))*(ixRight-ixLeft)); |
159 | if (ixNew == ixLeft || ixNew == ixRight) { |
160 | // Use bisection instead: |
161 | ixNew = (ixLeft+ixRight)/2; |
162 | } |
163 | if (!readIndexRecord(ixNew)) break; |
164 | unsigned long tsNew = tsPacketNumFromBuf(); |
165 | if (tsNew < tsPacketNumber) { |
166 | tsLeft = tsNew; |
167 | ixLeft = ixNew; |
168 | } else { |
169 | tsRight = tsNew; |
170 | ixRight = ixNew; |
171 | } |
172 | } |
173 | if (ixRight-ixLeft > 1 || tsPacketNumber <= tsLeft || tsPacketNumber > tsRight) break; // bad PCR values in index file? |
174 | |
175 | ixFound = ixRight; |
176 | if (reverseToPreviousCleanPoint) { |
177 | // "Rewind' until we reach the start of a Video Sequence or GOP header: |
178 | success = rewindToCleanPoint(ixFound); |
179 | } else { |
180 | success = True; |
181 | } |
182 | } while (0); |
183 | |
184 | if (success && readIndexRecord(ixFound)) { |
185 | // Return (and cache) information from record "ixFound": |
186 | pcr = fCachedPCR = pcrFromBuf(); |
187 | fCachedTSPacketNumber = tsPacketNumFromBuf(); |
188 | if (reverseToPreviousCleanPoint) tsPacketNumber = fCachedTSPacketNumber; |
189 | indexRecordNumber = fCachedIndexRecordNumber = ixFound; |
190 | } else { |
191 | // An error occurred: Return the default values, for tsPacketNumber == 0: |
192 | pcr = 0.0f; |
193 | indexRecordNumber = 0; |
194 | } |
195 | closeFid(); |
196 | } |
197 | |
198 | Boolean MPEG2TransportStreamIndexFile |
199 | ::readIndexRecordValues(unsigned long indexRecordNum, |
200 | unsigned long& transportPacketNum, u_int8_t& offset, |
201 | u_int8_t& size, float& pcr, u_int8_t& recordType) { |
202 | if (!readIndexRecord(indexRecordNum)) return False; |
203 | |
204 | transportPacketNum = tsPacketNumFromBuf(); |
205 | offset = offsetFromBuf(); |
206 | size = sizeFromBuf(); |
207 | pcr = pcrFromBuf(); |
208 | recordType = recordTypeFromBuf(); |
209 | return True; |
210 | } |
211 | |
212 | float MPEG2TransportStreamIndexFile::getPlayingDuration() { |
213 | if (fNumIndexRecords == 0 || !readOneIndexRecord(fNumIndexRecords-1)) return 0.0f; |
214 | |
215 | return pcrFromBuf(); |
216 | } |
217 | |
218 | int MPEG2TransportStreamIndexFile::mpegVersion() { |
219 | if (fMPEGVersion != 0) return fMPEGVersion; // we already know it |
220 | |
221 | // Read the first index record, and figure out the MPEG version from its type: |
222 | if (!readOneIndexRecord(0)) return 0; // unknown; perhaps the indecx file is empty? |
223 | |
224 | setMPEGVersionFromRecordType(recordTypeFromBuf()); |
225 | return fMPEGVersion; |
226 | } |
227 | |
228 | Boolean MPEG2TransportStreamIndexFile::openFid() { |
229 | if (fFid == NULL && fFileName != NULL) { |
230 | if ((fFid = OpenInputFile(envir(), fFileName)) != NULL) { |
231 | fCurrentIndexRecordNum = 0; |
232 | } |
233 | } |
234 | |
235 | return fFid != NULL; |
236 | } |
237 | |
238 | Boolean MPEG2TransportStreamIndexFile::seekToIndexRecord(unsigned long indexRecordNumber) { |
239 | if (!openFid()) return False; |
240 | |
241 | if (indexRecordNumber == fCurrentIndexRecordNum) return True; // we're already there |
242 | |
243 | if (SeekFile64(fFid, (int64_t)(indexRecordNumber*INDEX_RECORD_SIZE), SEEK_SET) != 0) return False; |
244 | fCurrentIndexRecordNum = indexRecordNumber; |
245 | return True; |
246 | } |
247 | |
248 | Boolean MPEG2TransportStreamIndexFile::readIndexRecord(unsigned long indexRecordNum) { |
249 | do { |
250 | if (!seekToIndexRecord(indexRecordNum)) break; |
251 | if (fread(fBuf, INDEX_RECORD_SIZE, 1, fFid) != 1) break; |
252 | ++fCurrentIndexRecordNum; |
253 | |
254 | return True; |
255 | } while (0); |
256 | |
257 | return False; // an error occurred |
258 | } |
259 | |
260 | Boolean MPEG2TransportStreamIndexFile::readOneIndexRecord(unsigned long indexRecordNum) { |
261 | Boolean result = readIndexRecord(indexRecordNum); |
262 | closeFid(); |
263 | |
264 | return result; |
265 | } |
266 | |
267 | void MPEG2TransportStreamIndexFile::closeFid() { |
268 | if (fFid != NULL) { |
269 | CloseInputFile(fFid); |
270 | fFid = NULL; |
271 | } |
272 | } |
273 | |
274 | float MPEG2TransportStreamIndexFile::pcrFromBuf() { |
275 | unsigned pcr_int = (fBuf[5]<<16) | (fBuf[4]<<8) | fBuf[3]; |
276 | u_int8_t pcr_frac = fBuf[6]; |
277 | return pcr_int + pcr_frac/256.0f; |
278 | } |
279 | |
280 | unsigned long MPEG2TransportStreamIndexFile::tsPacketNumFromBuf() { |
281 | return (fBuf[10]<<24) | (fBuf[9]<<16) | (fBuf[8]<<8) | fBuf[7]; |
282 | } |
283 | |
284 | void MPEG2TransportStreamIndexFile::setMPEGVersionFromRecordType(u_int8_t recordType) { |
285 | if (fMPEGVersion != 0) return; // we already know it |
286 | |
287 | u_int8_t const recordTypeWithoutStartBit = recordType&~0x80; |
288 | if (recordTypeWithoutStartBit >= 1 && recordTypeWithoutStartBit <= 4) fMPEGVersion = 2; |
289 | else if (recordTypeWithoutStartBit >= 5 && recordTypeWithoutStartBit <= 10) fMPEGVersion = 5; |
290 | // represents H.264 |
291 | else if (recordTypeWithoutStartBit >= 11 && recordTypeWithoutStartBit <= 16) fMPEGVersion = 6; |
292 | // represents H.265 |
293 | } |
294 | |
295 | Boolean MPEG2TransportStreamIndexFile::rewindToCleanPoint(unsigned long&ixFound) { |
296 | Boolean success = False; // until we learn otherwise |
297 | |
298 | while (ixFound > 0) { |
299 | if (!readIndexRecord(ixFound)) break; |
300 | |
301 | u_int8_t recordType = recordTypeFromBuf(); |
302 | setMPEGVersionFromRecordType(recordType); |
303 | |
304 | // A 'clean point' is the start of a 'frame' from which a decoder can cleanly resume |
305 | // handling the stream. For H.264, this is a SPS. For H.265, this is a VPS. |
306 | // For MPEG-2, this is a Video Sequence Header, or a GOP. |
307 | |
308 | if ((recordType&0x80) != 0) { // This is the start of a 'frame' |
309 | recordType &=~ 0x80; // remove the 'start of frame' bit |
310 | if (fMPEGVersion == 5) { // H.264 |
311 | if (recordType == 5/*SPS*/) { |
312 | success = True; |
313 | break; |
314 | } |
315 | } else if (fMPEGVersion == 6) { // H.265 |
316 | if (recordType == 11/*VPS*/) { |
317 | success = True; |
318 | break; |
319 | } |
320 | } else { // MPEG-1, 2, or 4 |
321 | if (recordType == 1/*VSH*/) { |
322 | success = True; |
323 | break; |
324 | } else if (recordType == 2/*GOP*/) { |
325 | // Hack: If the preceding record is for a Video Sequence Header, then use it instead: |
326 | unsigned long newIxFound = ixFound; |
327 | |
328 | while (--newIxFound > 0) { |
329 | if (!readIndexRecord(newIxFound)) break; |
330 | recordType = recordTypeFromBuf(); |
331 | if ((recordType&0x7F) != 1) break; // not a Video Sequence Header |
332 | if ((recordType&0x80) != 0) { // this is the start of the VSH; use it |
333 | ixFound = newIxFound; |
334 | break; |
335 | } |
336 | } |
337 | } |
338 | success = True; |
339 | break; |
340 | } |
341 | } |
342 | |
343 | // Keep checking, from the previous record: |
344 | --ixFound; |
345 | } |
346 | if (ixFound == 0) success = True; // use record 0 anyway |
347 | |
348 | return success; |
349 | } |
350 | |