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 | // 'Ogg' File Sink (recording a single media track only) |
19 | // Implementation |
20 | |
21 | #include "OggFileSink.hh" |
22 | #include "OutputFile.hh" |
23 | #include "VorbisAudioRTPSource.hh" // for "parseVorbisOrTheoraConfigStr()" |
24 | #include "MPEG2TransportStreamMultiplexor.hh" // for calculateCRC() |
25 | #include "FramedSource.hh" |
26 | |
27 | OggFileSink* OggFileSink |
28 | ::createNew(UsageEnvironment& env, char const* fileName, |
29 | unsigned samplingFrequency, char const* configStr, |
30 | unsigned bufferSize, Boolean oneFilePerFrame) { |
31 | do { |
32 | FILE* fid; |
33 | char const* perFrameFileNamePrefix; |
34 | if (oneFilePerFrame) { |
35 | // Create the fid for each frame |
36 | fid = NULL; |
37 | perFrameFileNamePrefix = fileName; |
38 | } else { |
39 | // Normal case: create the fid once |
40 | fid = OpenOutputFile(env, fileName); |
41 | if (fid == NULL) break; |
42 | perFrameFileNamePrefix = NULL; |
43 | } |
44 | |
45 | return new OggFileSink(env, fid, samplingFrequency, configStr, bufferSize, perFrameFileNamePrefix); |
46 | } while (0); |
47 | |
48 | return NULL; |
49 | } |
50 | |
51 | OggFileSink::OggFileSink(UsageEnvironment& env, FILE* fid, |
52 | unsigned samplingFrequency, char const* configStr, |
53 | unsigned bufferSize, char const* perFrameFileNamePrefix) |
54 | : FileSink(env, fid, bufferSize, perFrameFileNamePrefix), |
55 | fSamplingFrequency(samplingFrequency), fConfigStr(strDup(configStr)), |
56 | fHaveWrittenFirstFrame(False), fHaveSeenEOF(False), |
57 | fGranulePosition(0), fGranulePositionAdjustment(0), fPageSequenceNumber(0), |
58 | fIsTheora(False), fGranuleIncrementPerFrame(1), |
59 | fAltFrameSize(0), fAltNumTruncatedBytes(0) { |
60 | fAltBuffer = new unsigned char[bufferSize]; |
61 | |
62 | // Initialize our 'Ogg page header' array with constant values: |
63 | u_int8_t* p = fPageHeaderBytes; |
64 | *p++=0x4f; *p++=0x67; *p++=0x67; *p++=0x53; // bytes 0..3: 'capture_pattern': "OggS" |
65 | *p++=0; // byte 4: 'stream_structure_version': 0 |
66 | *p++=0; // byte 5: 'header_type_flag': set on each write |
67 | *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; |
68 | // bytes 6..13: 'granule_position': set on each write |
69 | *p++=1; *p++=0; *p++=0; *p++=0; // bytes 14..17: 'bitstream_serial_number': 1 |
70 | *p++=0; *p++=0; *p++=0; *p++=0; // bytes 18..21: 'page_sequence_number': set on each write |
71 | *p++=0; *p++=0; *p++=0; *p++=0; // bytes 22..25: 'CRC_checksum': set on each write |
72 | *p=0; // byte 26: 'number_page_segments': set on each write |
73 | } |
74 | |
75 | OggFileSink::~OggFileSink() { |
76 | // We still have the previously-arrived frame, so write it to the file before we end: |
77 | fHaveSeenEOF = True; |
78 | OggFileSink::addData(fAltBuffer, fAltFrameSize, fAltPresentationTime); |
79 | |
80 | delete[] fAltBuffer; |
81 | delete[] (char*)fConfigStr; |
82 | } |
83 | |
84 | Boolean OggFileSink::continuePlaying() { |
85 | // Identical to "FileSink::continuePlaying()", |
86 | // except that we use our own 'on source closure' function: |
87 | if (fSource == NULL) return False; |
88 | |
89 | fSource->getNextFrame(fBuffer, fBufferSize, |
90 | FileSink::afterGettingFrame, this, |
91 | ourOnSourceClosure, this); |
92 | return True; |
93 | } |
94 | |
95 | #define PAGE_DATA_MAX_SIZE (255*255) |
96 | |
97 | void OggFileSink::addData(unsigned char const* data, unsigned dataSize, |
98 | struct timeval presentationTime) { |
99 | if (dataSize == 0) return; |
100 | |
101 | // Set "fGranulePosition" for this frame: |
102 | if (fIsTheora) { |
103 | // Special case for Theora: "fGranulePosition" is supposed to be made up of a pair: |
104 | // (frame count to last key frame) | (frame count since last key frame) |
105 | // However, because there appears to be no easy way to figure out which frames are key frames, |
106 | // we just assume that all frames are key frames. |
107 | if (!(data[0] >= 0x80 && data[0] <= 0x82)) { // for header pages, "fGranulePosition" remains 0 |
108 | fGranulePosition += fGranuleIncrementPerFrame; |
109 | } |
110 | } else { |
111 | double ptDiff |
112 | = (presentationTime.tv_sec - fFirstPresentationTime.tv_sec) |
113 | + (presentationTime.tv_usec - fFirstPresentationTime.tv_usec)/1000000.0; |
114 | int64_t newGranulePosition |
115 | = (int64_t)(fSamplingFrequency*ptDiff) + fGranulePositionAdjustment; |
116 | if (newGranulePosition < fGranulePosition) { |
117 | // Update "fGranulePositionAdjustment" so that "fGranulePosition" remains monotonic |
118 | fGranulePositionAdjustment += fGranulePosition - newGranulePosition; |
119 | } else { |
120 | fGranulePosition = newGranulePosition; |
121 | } |
122 | } |
123 | |
124 | // Write the frame to the file as a single Ogg 'page' (or perhaps as multiple pages |
125 | // if it's too big for a single page). We don't aggregate more than one frame within |
126 | // an Ogg page because that's not legal for some headers, and because that would make |
127 | // it difficult for us to properly set the 'eos' (end of stream) flag on the last page. |
128 | |
129 | // First, figure out how many pages to write here |
130 | // (a page can contain no more than PAGE_DATA_MAX_SIZE bytes) |
131 | unsigned numPagesToWrite = dataSize/PAGE_DATA_MAX_SIZE + 1; |
132 | // Note that if "dataSize" is a integral multiple of PAGE_DATA_MAX_SIZE, there will |
133 | // be an extra 0-size page at the end |
134 | for (unsigned i = 0; i < numPagesToWrite; ++i) { |
135 | // First, fill in the changeable parts of our 'page header' array; |
136 | u_int8_t = 0x0; |
137 | if (!fHaveWrittenFirstFrame && i == 0) { |
138 | header_type_flag |= 0x02; // 'bos' |
139 | fHaveWrittenFirstFrame = True; // for the future |
140 | } |
141 | if (i > 0) header_type_flag |= 0x01; // 'continuation' |
142 | if (fHaveSeenEOF && i == numPagesToWrite-1) header_type_flag |= 0x04; // 'eos' |
143 | fPageHeaderBytes[5] = header_type_flag; |
144 | |
145 | if (i < numPagesToWrite-1) { |
146 | // For pages where the frame does not end, set 'granule_position' in the header to -1: |
147 | fPageHeaderBytes[6] = fPageHeaderBytes[7] = fPageHeaderBytes[8] = fPageHeaderBytes[9] = |
148 | fPageHeaderBytes[10] = fPageHeaderBytes[11] = fPageHeaderBytes[12] = fPageHeaderBytes[13] |
149 | = 0xFF; |
150 | } else { |
151 | fPageHeaderBytes[6] = (u_int8_t)fGranulePosition; |
152 | fPageHeaderBytes[7] = (u_int8_t)(fGranulePosition>>8); |
153 | fPageHeaderBytes[8] = (u_int8_t)(fGranulePosition>>16); |
154 | fPageHeaderBytes[9] = (u_int8_t)(fGranulePosition>>24); |
155 | fPageHeaderBytes[10] = (u_int8_t)(fGranulePosition>>32); |
156 | fPageHeaderBytes[11] = (u_int8_t)(fGranulePosition>>40); |
157 | fPageHeaderBytes[12] = (u_int8_t)(fGranulePosition>>48); |
158 | fPageHeaderBytes[13] = (u_int8_t)(fGranulePosition>>56); |
159 | } |
160 | |
161 | fPageHeaderBytes[18] = (u_int8_t)fPageSequenceNumber; |
162 | fPageHeaderBytes[19] = (u_int8_t)(fPageSequenceNumber>>8); |
163 | fPageHeaderBytes[20] = (u_int8_t)(fPageSequenceNumber>>16); |
164 | fPageHeaderBytes[21] = (u_int8_t)(fPageSequenceNumber>>24); |
165 | ++fPageSequenceNumber; |
166 | |
167 | unsigned pageDataSize; |
168 | u_int8_t number_page_segments; |
169 | if (dataSize >= PAGE_DATA_MAX_SIZE) { |
170 | pageDataSize = PAGE_DATA_MAX_SIZE; |
171 | number_page_segments = 255; |
172 | } else { |
173 | pageDataSize = dataSize; |
174 | number_page_segments = (pageDataSize+255)/255; // so that we don't end with a lacing of 255 |
175 | } |
176 | fPageHeaderBytes[26] = number_page_segments; |
177 | |
178 | u_int8_t segment_table[255]; |
179 | for (unsigned j = 0; j < (unsigned)(number_page_segments-1); ++j) { |
180 | segment_table[j] = 255; |
181 | } |
182 | segment_table[number_page_segments-1] = pageDataSize%255; |
183 | |
184 | // Compute the CRC from the 'page header' array, the 'segment_table', and the frame data: |
185 | u_int32_t crc = 0; |
186 | fPageHeaderBytes[22] = fPageHeaderBytes[23] = fPageHeaderBytes[24] = fPageHeaderBytes[25] = 0; |
187 | crc = calculateCRC(fPageHeaderBytes, 27, 0); |
188 | crc = calculateCRC(segment_table, number_page_segments, crc); |
189 | crc = calculateCRC(data, pageDataSize, crc); |
190 | fPageHeaderBytes[22] = (u_int8_t)crc; |
191 | fPageHeaderBytes[23] = (u_int8_t)(crc>>8); |
192 | fPageHeaderBytes[24] = (u_int8_t)(crc>>16); |
193 | fPageHeaderBytes[25] = (u_int8_t)(crc>>24); |
194 | |
195 | // Then write out the 'page header' array: |
196 | FileSink::addData(fPageHeaderBytes, 27, presentationTime); |
197 | |
198 | // Then write out the 'segment_table': |
199 | FileSink::addData(segment_table, number_page_segments, presentationTime); |
200 | |
201 | // Then add frame data, to complete the page: |
202 | FileSink::addData(data, pageDataSize, presentationTime); |
203 | data += pageDataSize; |
204 | dataSize -= pageDataSize; |
205 | } |
206 | } |
207 | |
208 | void OggFileSink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime) { |
209 | if (!fHaveWrittenFirstFrame) { |
210 | fFirstPresentationTime = presentationTime; |
211 | |
212 | // If we have a 'config string' representing 'packed configuration headers' |
213 | // ("identification", "comment", "setup"), unpack them and prepend them to the file: |
214 | if (fConfigStr != NULL && fConfigStr[0] != '\0') { |
215 | u_int8_t* identificationHdr; unsigned identificationHdrSize; |
216 | u_int8_t* ; unsigned ; |
217 | u_int8_t* setupHdr; unsigned setupHdrSize; |
218 | u_int32_t identField; |
219 | parseVorbisOrTheoraConfigStr(fConfigStr, |
220 | identificationHdr, identificationHdrSize, |
221 | commentHdr, commentHdrSize, |
222 | setupHdr, setupHdrSize, |
223 | identField); |
224 | if (identificationHdrSize >= 42 |
225 | && strncmp((const char*)&identificationHdr[1], "theora" , 6) == 0) { |
226 | // Hack for Theora video: Parse the "identification" hdr to get the "KFGSHIFT" parameter: |
227 | fIsTheora = True; |
228 | u_int8_t const KFGSHIFT = ((identificationHdr[40]&3)<<3) | (identificationHdr[41]>>5); |
229 | fGranuleIncrementPerFrame = (u_int64_t)(1 << KFGSHIFT); |
230 | } |
231 | OggFileSink::addData(identificationHdr, identificationHdrSize, presentationTime); |
232 | OggFileSink::addData(commentHdr, commentHdrSize, presentationTime); |
233 | |
234 | // Hack: Handle the "setup" header as if had arrived in the previous delivery, so it'll get |
235 | // written properly below: |
236 | if (setupHdrSize > fBufferSize) { |
237 | fAltFrameSize = fBufferSize; |
238 | fAltNumTruncatedBytes = setupHdrSize - fBufferSize; |
239 | } else { |
240 | fAltFrameSize = setupHdrSize; |
241 | fAltNumTruncatedBytes = 0; |
242 | } |
243 | memmove(fAltBuffer, setupHdr, fAltFrameSize); |
244 | fAltPresentationTime = presentationTime; |
245 | |
246 | delete[] identificationHdr; |
247 | delete[] commentHdr; |
248 | delete[] setupHdr; |
249 | } |
250 | } |
251 | |
252 | // Save this input frame for next time, and instead write the previous input frame now: |
253 | unsigned char* tmpPtr = fBuffer; fBuffer = fAltBuffer; fAltBuffer = tmpPtr; |
254 | unsigned prevFrameSize = fAltFrameSize; fAltFrameSize = frameSize; |
255 | unsigned prevNumTruncatedBytes = fAltNumTruncatedBytes; fAltNumTruncatedBytes = numTruncatedBytes; |
256 | struct timeval prevPresentationTime = fAltPresentationTime; fAltPresentationTime = presentationTime; |
257 | |
258 | // Call the parent class to complete the normal file write with the (previous) input frame: |
259 | FileSink::afterGettingFrame(prevFrameSize, prevNumTruncatedBytes, prevPresentationTime); |
260 | } |
261 | |
262 | void OggFileSink::ourOnSourceClosure(void* clientData) { |
263 | ((OggFileSink*)clientData)->ourOnSourceClosure(); |
264 | } |
265 | |
266 | void OggFileSink::ourOnSourceClosure() { |
267 | fHaveSeenEOF = True; |
268 | |
269 | // We still have the previously-arrived frame, so write it to the file before we end: |
270 | OggFileSink::addData(fAltBuffer, fAltFrameSize, fAltPresentationTime); |
271 | |
272 | // Handle the closure for real: |
273 | onSourceClosure(); |
274 | } |
275 | |