/********** 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. // 'Ogg' File Sink (recording a single media track only) // Implementation #include "OggFileSink.hh" #include "OutputFile.hh" #include "VorbisAudioRTPSource.hh" // for "parseVorbisOrTheoraConfigStr()" #include "MPEG2TransportStreamMultiplexor.hh" // for calculateCRC() #include "FramedSource.hh" OggFileSink* OggFileSink ::createNew(UsageEnvironment& env, char const* fileName, unsigned samplingFrequency, char const* configStr, unsigned bufferSize, Boolean oneFilePerFrame) { do { FILE* fid; char const* perFrameFileNamePrefix; if (oneFilePerFrame) { // Create the fid for each frame fid = NULL; perFrameFileNamePrefix = fileName; } else { // Normal case: create the fid once fid = OpenOutputFile(env, fileName); if (fid == NULL) break; perFrameFileNamePrefix = NULL; } return new OggFileSink(env, fid, samplingFrequency, configStr, bufferSize, perFrameFileNamePrefix); } while (0); return NULL; } OggFileSink::OggFileSink(UsageEnvironment& env, FILE* fid, unsigned samplingFrequency, char const* configStr, unsigned bufferSize, char const* perFrameFileNamePrefix) : FileSink(env, fid, bufferSize, perFrameFileNamePrefix), fSamplingFrequency(samplingFrequency), fConfigStr(strDup(configStr)), fHaveWrittenFirstFrame(False), fHaveSeenEOF(False), fGranulePosition(0), fGranulePositionAdjustment(0), fPageSequenceNumber(0), fIsTheora(False), fGranuleIncrementPerFrame(1), fAltFrameSize(0), fAltNumTruncatedBytes(0) { fAltBuffer = new unsigned char[bufferSize]; // Initialize our 'Ogg page header' array with constant values: u_int8_t* p = fPageHeaderBytes; *p++=0x4f; *p++=0x67; *p++=0x67; *p++=0x53; // bytes 0..3: 'capture_pattern': "OggS" *p++=0; // byte 4: 'stream_structure_version': 0 *p++=0; // byte 5: 'header_type_flag': set on each write *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; // bytes 6..13: 'granule_position': set on each write *p++=1; *p++=0; *p++=0; *p++=0; // bytes 14..17: 'bitstream_serial_number': 1 *p++=0; *p++=0; *p++=0; *p++=0; // bytes 18..21: 'page_sequence_number': set on each write *p++=0; *p++=0; *p++=0; *p++=0; // bytes 22..25: 'CRC_checksum': set on each write *p=0; // byte 26: 'number_page_segments': set on each write } OggFileSink::~OggFileSink() { // We still have the previously-arrived frame, so write it to the file before we end: fHaveSeenEOF = True; OggFileSink::addData(fAltBuffer, fAltFrameSize, fAltPresentationTime); delete[] fAltBuffer; delete[] (char*)fConfigStr; } Boolean OggFileSink::continuePlaying() { // Identical to "FileSink::continuePlaying()", // except that we use our own 'on source closure' function: if (fSource == NULL) return False; fSource->getNextFrame(fBuffer, fBufferSize, FileSink::afterGettingFrame, this, ourOnSourceClosure, this); return True; } #define PAGE_DATA_MAX_SIZE (255*255) void OggFileSink::addData(unsigned char const* data, unsigned dataSize, struct timeval presentationTime) { if (dataSize == 0) return; // Set "fGranulePosition" for this frame: if (fIsTheora) { // Special case for Theora: "fGranulePosition" is supposed to be made up of a pair: // (frame count to last key frame) | (frame count since last key frame) // However, because there appears to be no easy way to figure out which frames are key frames, // we just assume that all frames are key frames. if (!(data[0] >= 0x80 && data[0] <= 0x82)) { // for header pages, "fGranulePosition" remains 0 fGranulePosition += fGranuleIncrementPerFrame; } } else { double ptDiff = (presentationTime.tv_sec - fFirstPresentationTime.tv_sec) + (presentationTime.tv_usec - fFirstPresentationTime.tv_usec)/1000000.0; int64_t newGranulePosition = (int64_t)(fSamplingFrequency*ptDiff) + fGranulePositionAdjustment; if (newGranulePosition < fGranulePosition) { // Update "fGranulePositionAdjustment" so that "fGranulePosition" remains monotonic fGranulePositionAdjustment += fGranulePosition - newGranulePosition; } else { fGranulePosition = newGranulePosition; } } // Write the frame to the file as a single Ogg 'page' (or perhaps as multiple pages // if it's too big for a single page). We don't aggregate more than one frame within // an Ogg page because that's not legal for some headers, and because that would make // it difficult for us to properly set the 'eos' (end of stream) flag on the last page. // First, figure out how many pages to write here // (a page can contain no more than PAGE_DATA_MAX_SIZE bytes) unsigned numPagesToWrite = dataSize/PAGE_DATA_MAX_SIZE + 1; // Note that if "dataSize" is a integral multiple of PAGE_DATA_MAX_SIZE, there will // be an extra 0-size page at the end for (unsigned i = 0; i < numPagesToWrite; ++i) { // First, fill in the changeable parts of our 'page header' array; u_int8_t header_type_flag = 0x0; if (!fHaveWrittenFirstFrame && i == 0) { header_type_flag |= 0x02; // 'bos' fHaveWrittenFirstFrame = True; // for the future } if (i > 0) header_type_flag |= 0x01; // 'continuation' if (fHaveSeenEOF && i == numPagesToWrite-1) header_type_flag |= 0x04; // 'eos' fPageHeaderBytes[5] = header_type_flag; if (i < numPagesToWrite-1) { // For pages where the frame does not end, set 'granule_position' in the header to -1: fPageHeaderBytes[6] = fPageHeaderBytes[7] = fPageHeaderBytes[8] = fPageHeaderBytes[9] = fPageHeaderBytes[10] = fPageHeaderBytes[11] = fPageHeaderBytes[12] = fPageHeaderBytes[13] = 0xFF; } else { fPageHeaderBytes[6] = (u_int8_t)fGranulePosition; fPageHeaderBytes[7] = (u_int8_t)(fGranulePosition>>8); fPageHeaderBytes[8] = (u_int8_t)(fGranulePosition>>16); fPageHeaderBytes[9] = (u_int8_t)(fGranulePosition>>24); fPageHeaderBytes[10] = (u_int8_t)(fGranulePosition>>32); fPageHeaderBytes[11] = (u_int8_t)(fGranulePosition>>40); fPageHeaderBytes[12] = (u_int8_t)(fGranulePosition>>48); fPageHeaderBytes[13] = (u_int8_t)(fGranulePosition>>56); } fPageHeaderBytes[18] = (u_int8_t)fPageSequenceNumber; fPageHeaderBytes[19] = (u_int8_t)(fPageSequenceNumber>>8); fPageHeaderBytes[20] = (u_int8_t)(fPageSequenceNumber>>16); fPageHeaderBytes[21] = (u_int8_t)(fPageSequenceNumber>>24); ++fPageSequenceNumber; unsigned pageDataSize; u_int8_t number_page_segments; if (dataSize >= PAGE_DATA_MAX_SIZE) { pageDataSize = PAGE_DATA_MAX_SIZE; number_page_segments = 255; } else { pageDataSize = dataSize; number_page_segments = (pageDataSize+255)/255; // so that we don't end with a lacing of 255 } fPageHeaderBytes[26] = number_page_segments; u_int8_t segment_table[255]; for (unsigned j = 0; j < (unsigned)(number_page_segments-1); ++j) { segment_table[j] = 255; } segment_table[number_page_segments-1] = pageDataSize%255; // Compute the CRC from the 'page header' array, the 'segment_table', and the frame data: u_int32_t crc = 0; fPageHeaderBytes[22] = fPageHeaderBytes[23] = fPageHeaderBytes[24] = fPageHeaderBytes[25] = 0; crc = calculateCRC(fPageHeaderBytes, 27, 0); crc = calculateCRC(segment_table, number_page_segments, crc); crc = calculateCRC(data, pageDataSize, crc); fPageHeaderBytes[22] = (u_int8_t)crc; fPageHeaderBytes[23] = (u_int8_t)(crc>>8); fPageHeaderBytes[24] = (u_int8_t)(crc>>16); fPageHeaderBytes[25] = (u_int8_t)(crc>>24); // Then write out the 'page header' array: FileSink::addData(fPageHeaderBytes, 27, presentationTime); // Then write out the 'segment_table': FileSink::addData(segment_table, number_page_segments, presentationTime); // Then add frame data, to complete the page: FileSink::addData(data, pageDataSize, presentationTime); data += pageDataSize; dataSize -= pageDataSize; } } void OggFileSink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime) { if (!fHaveWrittenFirstFrame) { fFirstPresentationTime = presentationTime; // If we have a 'config string' representing 'packed configuration headers' // ("identification", "comment", "setup"), unpack them and prepend them to the file: if (fConfigStr != NULL && fConfigStr[0] != '\0') { u_int8_t* identificationHdr; unsigned identificationHdrSize; u_int8_t* commentHdr; unsigned commentHdrSize; u_int8_t* setupHdr; unsigned setupHdrSize; u_int32_t identField; parseVorbisOrTheoraConfigStr(fConfigStr, identificationHdr, identificationHdrSize, commentHdr, commentHdrSize, setupHdr, setupHdrSize, identField); if (identificationHdrSize >= 42 && strncmp((const char*)&identificationHdr[1], "theora", 6) == 0) { // Hack for Theora video: Parse the "identification" hdr to get the "KFGSHIFT" parameter: fIsTheora = True; u_int8_t const KFGSHIFT = ((identificationHdr[40]&3)<<3) | (identificationHdr[41]>>5); fGranuleIncrementPerFrame = (u_int64_t)(1 << KFGSHIFT); } OggFileSink::addData(identificationHdr, identificationHdrSize, presentationTime); OggFileSink::addData(commentHdr, commentHdrSize, presentationTime); // Hack: Handle the "setup" header as if had arrived in the previous delivery, so it'll get // written properly below: if (setupHdrSize > fBufferSize) { fAltFrameSize = fBufferSize; fAltNumTruncatedBytes = setupHdrSize - fBufferSize; } else { fAltFrameSize = setupHdrSize; fAltNumTruncatedBytes = 0; } memmove(fAltBuffer, setupHdr, fAltFrameSize); fAltPresentationTime = presentationTime; delete[] identificationHdr; delete[] commentHdr; delete[] setupHdr; } } // Save this input frame for next time, and instead write the previous input frame now: unsigned char* tmpPtr = fBuffer; fBuffer = fAltBuffer; fAltBuffer = tmpPtr; unsigned prevFrameSize = fAltFrameSize; fAltFrameSize = frameSize; unsigned prevNumTruncatedBytes = fAltNumTruncatedBytes; fAltNumTruncatedBytes = numTruncatedBytes; struct timeval prevPresentationTime = fAltPresentationTime; fAltPresentationTime = presentationTime; // Call the parent class to complete the normal file write with the (previous) input frame: FileSink::afterGettingFrame(prevFrameSize, prevNumTruncatedBytes, prevPresentationTime); } void OggFileSink::ourOnSourceClosure(void* clientData) { ((OggFileSink*)clientData)->ourOnSourceClosure(); } void OggFileSink::ourOnSourceClosure() { fHaveSeenEOF = True; // We still have the previously-arrived frame, so write it to the file before we end: OggFileSink::addData(fAltBuffer, fAltFrameSize, fAltPresentationTime); // Handle the closure for real: onSourceClosure(); }