// **************************************************************************** // // FLV Extract // Copyright (C) 2006-2009 Moitah (moitah@yahoo.com) // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // **************************************************************************** using System; using System.Collections.Generic; using System.Text; using System.IO; namespace JDP { interface IAudioWriter { void WriteChunk(byte[] chunk, uint timeStamp); void Finish(); string Path { get; } } interface IVideoWriter { void WriteChunk(byte[] chunk, uint timeStamp, int frameType); void Finish(FractionUInt32 averageFrameRate); string Path { get; } } public delegate bool OverwriteDelegate(string destPath); public class FLVFile : IDisposable { static readonly string[] _outputExtensions = new string[] { ".avi", ".mp3", ".264", ".aac", ".spx", ".txt" }; string _inputPath, _outputDirectory, _outputPathBase; OverwriteDelegate _overwrite; FileStream _fs; long _fileOffset, _fileLength; IAudioWriter _audioWriter; IVideoWriter _videoWriter; TimeCodeWriter _timeCodeWriter; List _videoTimeStamps; bool _extractAudio, _extractVideo, _extractTimeCodes; bool _extractedAudio, _extractedVideo, _extractedTimeCodes; FractionUInt32? _averageFrameRate, _trueFrameRate; List _warnings; public FLVFile(string path) { _inputPath = path; _outputDirectory = Path.GetDirectoryName(path); _warnings = new List(); _fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65536); _fileOffset = 0; _fileLength = _fs.Length; } public void Close() { Dispose(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (disposing) { if (_fs != null) { _fs.Close(); _fs = null; } CloseOutput(null, true); } } public string OutputDirectory { get { return _outputDirectory; } set { _outputDirectory = value; } } public FractionUInt32? AverageFrameRate { get { return _averageFrameRate; } } public FractionUInt32? TrueFrameRate { get { return _trueFrameRate; } } public string[] Warnings { get { return _warnings.ToArray(); } } public bool ExtractedAudio { get { return _extractedAudio; } } public bool ExtractedVideo { get { return _extractedVideo; } } public bool ExtractedTimeCodes { get { return _extractedTimeCodes; } } public void ExtractStreams(bool extractAudio, bool extractVideo, bool extractTimeCodes, OverwriteDelegate overwrite) { uint dataOffset, flags, prevTagSize; _outputPathBase = Path.Combine(_outputDirectory, Path.GetFileNameWithoutExtension(_inputPath)); _overwrite = overwrite; _extractAudio = extractAudio; _extractVideo = extractVideo; _extractTimeCodes = extractTimeCodes; _videoTimeStamps = new List(); Seek(0); if (ReadUInt32() != 0x464C5601) { throw new Exception("This isn't a FLV file."); } if (Array.IndexOf(_outputExtensions, Path.GetExtension(_inputPath).ToLower()) != -1) { // Can't have the same extension as files we output throw new Exception("Please change the extension of this FLV file."); } if (!Directory.Exists(_outputDirectory)) { throw new Exception("Output directory doesn't exist."); } flags = ReadUInt8(); dataOffset = ReadUInt32(); Seek(dataOffset); prevTagSize = ReadUInt32(); while (_fileOffset < _fileLength) { if (!ReadTag()) break; if ((_fileLength - _fileOffset) < 4) break; prevTagSize = ReadUInt32(); } _averageFrameRate = CalculateAverageFrameRate(); _trueFrameRate = CalculateTrueFrameRate(); CloseOutput(_averageFrameRate, false); } private void CloseOutput(FractionUInt32? averageFrameRate, bool disposing) { if (_videoWriter != null) { _videoWriter.Finish(averageFrameRate ?? new FractionUInt32(25, 1)); if (disposing && (_videoWriter.Path != null)) { try { File.Delete(_videoWriter.Path); } catch { } } _videoWriter = null; } if (_audioWriter != null) { _audioWriter.Finish(); if (disposing && (_audioWriter.Path != null)) { try { File.Delete(_audioWriter.Path); } catch { } } _audioWriter = null; } if (_timeCodeWriter != null) { _timeCodeWriter.Finish(); if (disposing && (_timeCodeWriter.Path != null)) { try { File.Delete(_timeCodeWriter.Path); } catch { } } _timeCodeWriter = null; } } private bool ReadTag() { uint tagType, dataSize, timeStamp, streamID, mediaInfo; byte[] data; if ((_fileLength - _fileOffset) < 11) { return false; } // Read tag header tagType = ReadUInt8(); dataSize = ReadUInt24(); timeStamp = ReadUInt24(); timeStamp |= ReadUInt8() << 24; streamID = ReadUInt24(); // Read tag data if (dataSize == 0) { return true; } if ((_fileLength - _fileOffset) < dataSize) { return false; } mediaInfo = ReadUInt8(); dataSize -= 1; data = ReadBytes((int)dataSize); if (tagType == 0x8) { // Audio if (_audioWriter == null) { _audioWriter = _extractAudio ? GetAudioWriter(mediaInfo) : new DummyAudioWriter(); _extractedAudio = !(_audioWriter is DummyAudioWriter); } _audioWriter.WriteChunk(data, timeStamp); } else if ((tagType == 0x9) && ((mediaInfo >> 4) != 5)) { // Video if (_videoWriter == null) { _videoWriter = _extractVideo ? GetVideoWriter(mediaInfo) : new DummyVideoWriter(); _extractedVideo = !(_videoWriter is DummyVideoWriter); } if (_timeCodeWriter == null) { string path = _outputPathBase + ".txt"; _timeCodeWriter = new TimeCodeWriter((_extractTimeCodes && CanWriteTo(path)) ? path : null); _extractedTimeCodes = _extractTimeCodes; } _videoTimeStamps.Add(timeStamp); _videoWriter.WriteChunk(data, timeStamp, (int)((mediaInfo & 0xF0) >> 4)); _timeCodeWriter.Write(timeStamp); } return true; } private IAudioWriter GetAudioWriter(uint mediaInfo) { uint format = mediaInfo >> 4; uint rate = (mediaInfo >> 2) & 0x3; uint bits = (mediaInfo >> 1) & 0x1; uint chans = mediaInfo & 0x1; string path; if ((format == 2) || (format == 14)) { // MP3 path = _outputPathBase + ".mp3"; if (!CanWriteTo(path)) return new DummyAudioWriter(); return new MP3Writer(path, _warnings); } else if ((format == 0) || (format == 3)) { // PCM int sampleRate = 0; switch (rate) { case 0: sampleRate = 5512; break; case 1: sampleRate = 11025; break; case 2: sampleRate = 22050; break; case 3: sampleRate = 44100; break; } path = _outputPathBase + ".wav"; if (!CanWriteTo(path)) return new DummyAudioWriter(); if (format == 0) { _warnings.Add("PCM byte order unspecified, assuming little endian."); } return new WAVWriter(path, (bits == 1) ? 16 : 8, (chans == 1) ? 2 : 1, sampleRate); } else if (format == 10) { // AAC path = _outputPathBase + ".aac"; if (!CanWriteTo(path)) return new DummyAudioWriter(); return new AACWriter(path); } else if (format == 11) { // Speex path = _outputPathBase + ".spx"; if (!CanWriteTo(path)) return new DummyAudioWriter(); return new SpeexWriter(path, (int)(_fileLength & 0xFFFFFFFF)); } else { string typeStr; if (format == 1) typeStr = "ADPCM"; else if ((format == 4) || (format == 5) || (format == 6)) typeStr = "Nellymoser"; else typeStr = "format=" + format.ToString(); _warnings.Add("Unable to extract audio (" + typeStr + " is unsupported)."); return new DummyAudioWriter(); } } private IVideoWriter GetVideoWriter(uint mediaInfo) { uint codecID = mediaInfo & 0x0F; string path; if ((codecID == 2) || (codecID == 4) || (codecID == 5)) { path = _outputPathBase + ".avi"; if (!CanWriteTo(path)) return new DummyVideoWriter(); return new AVIWriter(path, (int)codecID, _warnings); } else if (codecID == 7) { path = _outputPathBase + ".264"; if (!CanWriteTo(path)) return new DummyVideoWriter(); return new RawH264Writer(path); } else { string typeStr; if (codecID == 3) typeStr = "Screen"; else if (codecID == 6) typeStr = "Screen2"; else typeStr = "codecID=" + codecID.ToString(); _warnings.Add("Unable to extract video (" + typeStr + " is unsupported)."); return new DummyVideoWriter(); } } private bool CanWriteTo(string path) { if (File.Exists(path) && (_overwrite != null)) { return _overwrite(path); } return true; } private FractionUInt32? CalculateAverageFrameRate() { FractionUInt32 frameRate; int frameCount = _videoTimeStamps.Count; if (frameCount > 1) { frameRate.N = (uint)(frameCount - 1) * 1000; frameRate.D = _videoTimeStamps[frameCount - 1] - _videoTimeStamps[0]; frameRate.Reduce(); return frameRate; } else { return null; } } private FractionUInt32? CalculateTrueFrameRate() { FractionUInt32 frameRate; Dictionary deltaCount = new Dictionary(); int i, threshold; uint delta, count, minDelta; // Calculate the distance between the timestamps, count how many times each delta appears for (i = 1; i < _videoTimeStamps.Count; i++) { int deltaS = (int)((long)_videoTimeStamps[i] - (long)_videoTimeStamps[i - 1]); if (deltaS <= 0) continue; delta = (uint)deltaS; if (deltaCount.ContainsKey(delta)) { deltaCount[delta] += 1; } else { deltaCount.Add(delta, 1); } } threshold = _videoTimeStamps.Count / 10; minDelta = UInt32.MaxValue; // Find the smallest delta that made up at least 10% of the frames (grouping in delta+1 // because of rounding, e.g. a NTSC video will have deltas of 33 and 34 ms) foreach (KeyValuePair deltaItem in deltaCount) { delta = deltaItem.Key; count = deltaItem.Value; if (deltaCount.ContainsKey(delta + 1)) { count += deltaCount[delta + 1]; } if ((count >= threshold) && (delta < minDelta)) { minDelta = delta; } } // Calculate the frame rate based on the smallest delta, and delta+1 if present if (minDelta != UInt32.MaxValue) { uint totalTime, totalFrames; count = deltaCount[minDelta]; totalTime = minDelta * count; totalFrames = count; if (deltaCount.ContainsKey(minDelta + 1)) { count = deltaCount[minDelta + 1]; totalTime += (minDelta + 1) * count; totalFrames += count; } if (totalTime != 0) { frameRate.N = totalFrames * 1000; frameRate.D = totalTime; frameRate.Reduce(); return frameRate; } } // Unable to calculate frame rate return null; } private void Seek(long offset) { _fs.Seek(offset, SeekOrigin.Begin); _fileOffset = offset; } private uint ReadUInt8() { _fileOffset += 1; return (uint)_fs.ReadByte(); } private uint ReadUInt24() { byte[] x = new byte[4]; _fs.Read(x, 1, 3); _fileOffset += 3; return BitConverterBE.ToUInt32(x, 0); } private uint ReadUInt32() { byte[] x = new byte[4]; _fs.Read(x, 0, 4); _fileOffset += 4; return BitConverterBE.ToUInt32(x, 0); } private byte[] ReadBytes(int length) { byte[] buff = new byte[length]; _fs.Read(buff, 0, length); _fileOffset += length; return buff; } } class DummyAudioWriter : IAudioWriter { public DummyAudioWriter() { } public void WriteChunk(byte[] chunk, uint timeStamp) { } public void Finish() { } public string Path { get { return null; } } } class DummyVideoWriter : IVideoWriter { public DummyVideoWriter() { } public void WriteChunk(byte[] chunk, uint timeStamp, int frameType) { } public void Finish(FractionUInt32 averageFrameRate) { } public string Path { get { return null; } } } class MP3Writer : IAudioWriter { string _path; FileStream _fs; List _warnings; List _chunkBuffer; List _frameOffsets; uint _totalFrameLength; bool _isVBR; bool _delayWrite; bool _hasVBRHeader; bool _writeVBRHeader; int _firstBitRate; int _mpegVersion; int _sampleRate; int _channelMode; uint _firstFrameHeader; public MP3Writer(string path, List warnings) { _path = path; _fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 65536); _warnings = warnings; _chunkBuffer = new List(); _frameOffsets = new List(); _delayWrite = true; } public void WriteChunk(byte[] chunk, uint timeStamp) { _chunkBuffer.Add(chunk); ParseMP3Frames(chunk); if (_delayWrite && _totalFrameLength >= 65536) { _delayWrite = false; } if (!_delayWrite) { foreach (byte[] c in _chunkBuffer) { _fs.Write(c, 0, c.Length); } _chunkBuffer.Clear(); } } public void Finish() { if (_writeVBRHeader) { _fs.Seek(0, SeekOrigin.Begin); WriteVBRHeader(false); } _fs.Close(); } public string Path { get { return _path; } } private void ParseMP3Frames(byte[] buff) { int[] MPEG1BitRate = new int[] { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }; int[] MPEG2XBitRate = new int[] { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }; int[] MPEG1SampleRate = new int[] { 44100, 48000, 32000, 0 }; int[] MPEG20SampleRate = new int[] { 22050, 24000, 16000, 0 }; int[] MPEG25SampleRate = new int[] { 11025, 12000, 8000, 0 }; int offset = 0; int length = buff.Length; while (length >= 4) { ulong header; int mpegVersion, layer, bitRate, sampleRate, padding, channelMode; int frameLen; header = (ulong)BitConverterBE.ToUInt32(buff, offset) << 32; if (BitHelper.Read(ref header, 11) != 0x7FF) { break; } mpegVersion = BitHelper.Read(ref header, 2); layer = BitHelper.Read(ref header, 2); BitHelper.Read(ref header, 1); bitRate = BitHelper.Read(ref header, 4); sampleRate = BitHelper.Read(ref header, 2); padding = BitHelper.Read(ref header, 1); BitHelper.Read(ref header, 1); channelMode = BitHelper.Read(ref header, 2); if ((mpegVersion == 1) || (layer != 1) || (bitRate == 0) || (bitRate == 15) || (sampleRate == 3)) { break; } bitRate = ((mpegVersion == 3) ? MPEG1BitRate[bitRate] : MPEG2XBitRate[bitRate]) * 1000; if (mpegVersion == 3) sampleRate = MPEG1SampleRate[sampleRate]; else if (mpegVersion == 2) sampleRate = MPEG20SampleRate[sampleRate]; else sampleRate = MPEG25SampleRate[sampleRate]; frameLen = GetFrameLength(mpegVersion, bitRate, sampleRate, padding); if (frameLen > length) { break; } bool isVBRHeaderFrame = false; if (_frameOffsets.Count == 0) { // Check for an existing VBR header just to be safe (I haven't seen any in FLVs) int o = offset + GetFrameDataOffset(mpegVersion, channelMode); if (BitConverterBE.ToUInt32(buff, o) == 0x58696E67) { // "Xing" isVBRHeaderFrame = true; _delayWrite = false; _hasVBRHeader = true; } } if (isVBRHeaderFrame) { } else if (_firstBitRate == 0) { _firstBitRate = bitRate; _mpegVersion = mpegVersion; _sampleRate = sampleRate; _channelMode = channelMode; _firstFrameHeader = BitConverterBE.ToUInt32(buff, offset); } else if (!_isVBR && (bitRate != _firstBitRate)) { _isVBR = true; if (_hasVBRHeader) { } else if (_delayWrite) { WriteVBRHeader(true); _writeVBRHeader = true; _delayWrite = false; } else { _warnings.Add("Detected VBR too late, cannot add VBR header."); } } _frameOffsets.Add(_totalFrameLength + (uint)offset); offset += frameLen; length -= frameLen; } _totalFrameLength += (uint)buff.Length; } private void WriteVBRHeader(bool isPlaceholder) { byte[] buff = new byte[GetFrameLength(_mpegVersion, 64000, _sampleRate, 0)]; if (!isPlaceholder) { uint header = _firstFrameHeader; int dataOffset = GetFrameDataOffset(_mpegVersion, _channelMode); header &= 0xFFFE0DFF; // Clear CRC, bitrate, and padding fields header |= (uint)((_mpegVersion == 3) ? 5 : 8) << 12; // 64 kbit/sec General.CopyBytes(buff, 0, BitConverterBE.GetBytes(header)); General.CopyBytes(buff, dataOffset, BitConverterBE.GetBytes((uint)0x58696E67)); // "Xing" General.CopyBytes(buff, dataOffset + 4, BitConverterBE.GetBytes((uint)0x7)); // Flags General.CopyBytes(buff, dataOffset + 8, BitConverterBE.GetBytes((uint)_frameOffsets.Count)); // Frame count General.CopyBytes(buff, dataOffset + 12, BitConverterBE.GetBytes((uint)_totalFrameLength)); // File length for (int i = 0; i < 100; i++) { int frameIndex = (int)((i / 100.0) * _frameOffsets.Count); buff[dataOffset + 16 + i] = (byte)((_frameOffsets[frameIndex] / (double)_totalFrameLength) * 256.0); } } _fs.Write(buff, 0, buff.Length); } private int GetFrameLength(int mpegVersion, int bitRate, int sampleRate, int padding) { return ((mpegVersion == 3) ? 144 : 72) * bitRate / sampleRate + padding; } private int GetFrameDataOffset(int mpegVersion, int channelMode) { return 4 + ((mpegVersion == 3) ? ((channelMode == 3) ? 17 : 32) : ((channelMode == 3) ? 9 : 17)); } } class SpeexWriter : IAudioWriter { const string _vendorString = "FLV Extract"; const uint _sampleRate = 16000; const uint _msPerFrame = 20; const uint _samplesPerFrame = _sampleRate / (1000 / _msPerFrame); const int _targetPageDataSize = 4096; string _path; FileStream _fs; int _serialNumber; List _packetList; int _packetListDataSize; byte[] _pageBuff; int _pageBuffOffset; uint _pageSequenceNumber; ulong _granulePosition; public SpeexWriter(string path, int serialNumber) { _path = path; _serialNumber = serialNumber; _fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 65536); _fs.Seek((28 + 80) + (28 + 8 + _vendorString.Length), SeekOrigin.Begin); // Speex header + Vorbis comment _packetList = new List(); _packetListDataSize = 0; _pageBuff = new byte[27 + 255 + _targetPageDataSize + 254]; // Header + max segment table + target data size + extra segment _pageBuffOffset = 0; _pageSequenceNumber = 2; // First audio packet _granulePosition = 0; } public void WriteChunk(byte[] chunk, uint timeStamp) { int[] subModeSizes = new int[] { 0, 43, 119, 160, 220, 300, 364, 492, 79 }; int[] wideBandSizes = new int[] { 0, 36, 112, 192, 352 }; int[] inBandSignalSizes = new int[] { 1, 1, 4, 4, 4, 4, 4, 4, 8, 8, 16, 16, 32, 32, 64, 64 }; int frameStart = -1; int frameEnd = 0; int offset = 0; int length = chunk.Length * 8; int x; while (length - offset >= 5) { x = BitHelper.Read(chunk, ref offset, 1); if (x != 0) { // wideband frame x = BitHelper.Read(chunk, ref offset, 3); if (x < 1 || x > 4) goto Error; offset += wideBandSizes[x] - 4; } else { x = BitHelper.Read(chunk, ref offset, 4); if (x >= 1 && x <= 8) { // narrowband frame if (frameStart != -1) { WriteFramePacket(chunk, frameStart, frameEnd); } frameStart = frameEnd; offset += subModeSizes[x] - 5; } else if (x == 15) { // terminator break; } else if (x == 14) { // in-band signal if (length - offset < 4) goto Error; x = BitHelper.Read(chunk, ref offset, 4); offset += inBandSignalSizes[x]; } else if (x == 13) { // custom in-band signal if (length - offset < 5) goto Error; x = BitHelper.Read(chunk, ref offset, 5); offset += x * 8; } else goto Error; } frameEnd = offset; } if (offset > length) goto Error; if (frameStart != -1) { WriteFramePacket(chunk, frameStart, frameEnd); } return; Error: throw new Exception("Invalid Speex data."); } public void Finish() { WritePage(); FlushPage(true); _fs.Seek(0, SeekOrigin.Begin); _pageSequenceNumber = 0; _granulePosition = 0; WriteSpeexHeaderPacket(); WriteVorbisCommentPacket(); FlushPage(false); _fs.Close(); } public string Path { get { return _path; } } private void WriteFramePacket(byte[] data, int startBit, int endBit) { int lengthBits = endBit - startBit; byte[] frame = BitHelper.CopyBlock(data, startBit, lengthBits); if (lengthBits % 8 != 0) { frame[frame.Length - 1] |= (byte)(0xFF >> ((lengthBits % 8) + 1)); // padding } AddPacket(frame, _samplesPerFrame, true); } private void WriteSpeexHeaderPacket() { byte[] data = new byte[80]; General.CopyBytes(data, 0, Encoding.ASCII.GetBytes("Speex ")); // speex_string General.CopyBytes(data, 8, Encoding.ASCII.GetBytes("unknown")); // speex_version data[28] = 1; // speex_version_id data[32] = 80; // header_size General.CopyBytes(data, 36, BitConverterLE.GetBytes((uint)_sampleRate)); // rate data[40] = 1; // mode (e.g. narrowband, wideband) data[44] = 4; // mode_bitstream_version data[48] = 1; // nb_channels General.CopyBytes(data, 52, BitConverterLE.GetBytes(unchecked((uint)-1))); // bitrate General.CopyBytes(data, 56, BitConverterLE.GetBytes((uint)_samplesPerFrame)); // frame_size data[60] = 0; // vbr data[64] = 1; // frames_per_packet AddPacket(data, 0, false); } private void WriteVorbisCommentPacket() { byte[] vendorStringBytes = Encoding.ASCII.GetBytes(_vendorString); byte[] data = new byte[8 + vendorStringBytes.Length]; data[0] = (byte)vendorStringBytes.Length; General.CopyBytes(data, 4, vendorStringBytes); AddPacket(data, 0, false); } private void AddPacket(byte[] data, uint sampleLength, bool delayWrite) { OggPacket packet = new OggPacket(); if (data.Length >= 255) { throw new Exception("Packet exceeds maximum size."); } _granulePosition += sampleLength; packet.Data = data; packet.GranulePosition = _granulePosition; _packetList.Add(packet); _packetListDataSize += data.Length; if (!delayWrite || (_packetListDataSize >= _targetPageDataSize) || (_packetList.Count == 255)) { WritePage(); } } private void WritePage() { if (_packetList.Count == 0) return; FlushPage(false); WriteToPage(BitConverterBE.GetBytes(0x4F676753U), 0, 4); // "OggS" WriteToPage((byte)0); // Stream structure version WriteToPage((byte)((_pageSequenceNumber == 0) ? 0x02 : 0)); // Page flags WriteToPage((ulong)_packetList[_packetList.Count - 1].GranulePosition); // Position in samples WriteToPage((uint)_serialNumber); // Stream serial number WriteToPage((uint)_pageSequenceNumber); // Page sequence number WriteToPage((uint)0); // Checksum WriteToPage((byte)_packetList.Count); // Page segment count foreach (OggPacket packet in _packetList) { WriteToPage((byte)packet.Data.Length); } foreach (OggPacket packet in _packetList) { WriteToPage(packet.Data, 0, packet.Data.Length); } _packetList.Clear(); _packetListDataSize = 0; _pageSequenceNumber++; } private void FlushPage(bool isLastPage) { if (_pageBuffOffset == 0) return; if (isLastPage) _pageBuff[5] |= 0x04; uint crc = OggCRC.Calculate(_pageBuff, 0, _pageBuffOffset); General.CopyBytes(_pageBuff, 22, BitConverterLE.GetBytes(crc)); _fs.Write(_pageBuff, 0, _pageBuffOffset); _pageBuffOffset = 0; } private void WriteToPage(byte[] data, int offset, int length) { Buffer.BlockCopy(data, offset, _pageBuff, _pageBuffOffset, length); _pageBuffOffset += length; } private void WriteToPage(byte data) { WriteToPage(new byte[] { data }, 0, 1); } private void WriteToPage(uint data) { WriteToPage(BitConverterLE.GetBytes(data), 0, 4); } private void WriteToPage(ulong data) { WriteToPage(BitConverterLE.GetBytes(data), 0, 8); } class OggPacket { public ulong GranulePosition; public byte[] Data; } } class AACWriter : IAudioWriter { string _path; FileStream _fs; int _aacProfile; int _sampleRateIndex; int _channelConfig; public AACWriter(string path) { _path = path; _fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 65536); } public void WriteChunk(byte[] chunk, uint timeStamp) { if (chunk.Length < 1) return; if (chunk[0] == 0) { // Header if (chunk.Length < 3) return; ulong bits = (ulong)BitConverterBE.ToUInt16(chunk, 1) << 48; _aacProfile = BitHelper.Read(ref bits, 5) - 1; _sampleRateIndex = BitHelper.Read(ref bits, 4); _channelConfig = BitHelper.Read(ref bits, 4); if ((_aacProfile < 0) || (_aacProfile > 3)) throw new Exception("Unsupported AAC profile."); if (_sampleRateIndex > 12) throw new Exception("Invalid AAC sample rate index."); if (_channelConfig > 6) throw new Exception("Invalid AAC channel configuration."); } else { // Audio data int dataSize = chunk.Length - 1; ulong bits = 0; // Reference: WriteADTSHeader from FAAC's bitstream.c BitHelper.Write(ref bits, 12, 0xFFF); BitHelper.Write(ref bits, 1, 0); BitHelper.Write(ref bits, 2, 0); BitHelper.Write(ref bits, 1, 1); BitHelper.Write(ref bits, 2, _aacProfile); BitHelper.Write(ref bits, 4, _sampleRateIndex); BitHelper.Write(ref bits, 1, 0); BitHelper.Write(ref bits, 3, _channelConfig); BitHelper.Write(ref bits, 1, 0); BitHelper.Write(ref bits, 1, 0); BitHelper.Write(ref bits, 1, 0); BitHelper.Write(ref bits, 1, 0); BitHelper.Write(ref bits, 13, 7 + dataSize); BitHelper.Write(ref bits, 11, 0x7FF); BitHelper.Write(ref bits, 2, 0); _fs.Write(BitConverterBE.GetBytes(bits), 1, 7); _fs.Write(chunk, 1, dataSize); } } public void Finish() { _fs.Close(); } public string Path { get { return _path; } } } class RawH264Writer : IVideoWriter { static readonly byte[] _startCode = new byte[] { 0, 0, 0, 1 }; string _path; FileStream _fs; int _nalLengthSize; public RawH264Writer(string path) { _path = path; _fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 65536); } public void WriteChunk(byte[] chunk, uint timeStamp, int frameType) { if (chunk.Length < 4) return; // Reference: decode_frame from libavcodec's h264.c if (chunk[0] == 0) { // Headers if (chunk.Length < 10) return; int offset, spsCount, ppsCount; offset = 8; _nalLengthSize = (chunk[offset++] & 0x03) + 1; spsCount = chunk[offset++] & 0x1F; ppsCount = -1; while (offset <= chunk.Length - 2) { if ((spsCount == 0) && (ppsCount == -1)) { ppsCount = chunk[offset++]; continue; } if (spsCount > 0) spsCount--; else if (ppsCount > 0) ppsCount--; else break; int len = (int)BitConverterBE.ToUInt16(chunk, offset); offset += 2; if (offset + len > chunk.Length) break; _fs.Write(_startCode, 0, _startCode.Length); _fs.Write(chunk, offset, len); offset += len; } } else { // Video data int offset = 4; if (_nalLengthSize != 2) { _nalLengthSize = 4; } while (offset <= chunk.Length - _nalLengthSize) { int len = (_nalLengthSize == 2) ? (int)BitConverterBE.ToUInt16(chunk, offset) : (int)BitConverterBE.ToUInt32(chunk, offset); offset += _nalLengthSize; if (offset + len > chunk.Length) break; _fs.Write(_startCode, 0, _startCode.Length); _fs.Write(chunk, offset, len); offset += len; } } } public void Finish(FractionUInt32 averageFrameRate) { _fs.Close(); } public string Path { get { return _path; } } } class WAVWriter : IAudioWriter { string _path; WAVTools.WAVWriter _wr; int blockAlign; public WAVWriter(string path, int bitsPerSample, int channelCount, int sampleRate) { _path = path; _wr = new WAVTools.WAVWriter(path, bitsPerSample, channelCount, sampleRate); blockAlign = (bitsPerSample / 8) * channelCount; } public void WriteChunk(byte[] chunk, uint timeStamp) { _wr.Write(chunk, chunk.Length / blockAlign); } public void Finish() { _wr.Close(); } public string Path { get { return _path; } } } class AVIWriter : IVideoWriter { string _path; BinaryWriter _bw; int _codecID; int _width, _height, _frameCount; uint _moviDataSize, _indexChunkSize; List _index; bool _isAlphaWriter; AVIWriter _alphaWriter; List _warnings; // Chunk: Off: Len: // // RIFF AVI 0 12 // LIST hdrl 12 12 // avih 24 64 // LIST strl 88 12 // strh 100 64 // strf 164 48 // LIST movi 212 12 // (frames) 224 ??? // idx1 ??? ??? public AVIWriter(string path, int codecID, List warnings) : this(path, codecID, warnings, false) { } private AVIWriter(string path, int codecID, List warnings, bool isAlphaWriter) { if ((codecID != 2) && (codecID != 4) && (codecID != 5)) { throw new Exception("Unsupported video codec."); } FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); _path = path; _bw = new BinaryWriter(fs); _codecID = codecID; _warnings = warnings; _isAlphaWriter = isAlphaWriter; if ((codecID == 5) && !_isAlphaWriter) { _alphaWriter = new AVIWriter(path.Substring(0, path.Length - 4) + ".alpha.avi", codecID, warnings, true); } WriteFourCC("RIFF"); _bw.Write((uint)0); // chunk size WriteFourCC("AVI "); WriteFourCC("LIST"); _bw.Write((uint)192); WriteFourCC("hdrl"); WriteFourCC("avih"); _bw.Write((uint)56); _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0x10); _bw.Write((uint)0); // frame count _bw.Write((uint)0); _bw.Write((uint)1); _bw.Write((uint)0); _bw.Write((uint)0); // width _bw.Write((uint)0); // height _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); WriteFourCC("LIST"); _bw.Write((uint)116); WriteFourCC("strl"); WriteFourCC("strh"); _bw.Write((uint)56); WriteFourCC("vids"); WriteFourCC(CodecFourCC); _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); // frame rate denominator _bw.Write((uint)0); // frame rate numerator _bw.Write((uint)0); _bw.Write((uint)0); // frame count _bw.Write((uint)0); _bw.Write((int)-1); _bw.Write((uint)0); _bw.Write((ushort)0); _bw.Write((ushort)0); _bw.Write((ushort)0); // width _bw.Write((ushort)0); // height WriteFourCC("strf"); _bw.Write((uint)40); _bw.Write((uint)40); _bw.Write((uint)0); // width _bw.Write((uint)0); // height _bw.Write((ushort)1); _bw.Write((ushort)24); WriteFourCC(CodecFourCC); _bw.Write((uint)0); // biSizeImage _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); _bw.Write((uint)0); WriteFourCC("LIST"); _bw.Write((uint)0); // chunk size WriteFourCC("movi"); _index = new List(); } public void WriteChunk(byte[] chunk, uint timeStamp, int frameType) { int offset, len; offset = 0; len = chunk.Length; if (_codecID == 4) { offset = 1; len -= 1; } if (_codecID == 5) { offset = 4; if (len >= 4) { int alphaOffset = (int)BitConverterBE.ToUInt32(chunk, 0) & 0xFFFFFF; if (!_isAlphaWriter) { len = alphaOffset; } else { offset += alphaOffset; len -= offset; } } else { len = 0; } } len = Math.Max(len, 0); len = Math.Min(len, chunk.Length - offset); _index.Add((frameType == 1) ? (uint)0x10 : (uint)0); _index.Add(_moviDataSize + 4); _index.Add((uint)len); if ((_width == 0) && (_height == 0)) { GetFrameSize(chunk); } WriteFourCC("00dc"); _bw.Write(len); _bw.Write(chunk, offset, len); if ((len % 2) != 0) { _bw.Write((byte)0); len++; } _moviDataSize += (uint)len + 8; _frameCount++; if (_alphaWriter != null) { _alphaWriter.WriteChunk(chunk, timeStamp, frameType); } } private void GetFrameSize(byte[] chunk) { if (_codecID == 2) { // Reference: flv_h263_decode_picture_header from libavcodec's h263.c if (chunk.Length < 10) return; if ((chunk[0] != 0) || (chunk[1] != 0)) { return; } ulong x = BitConverterBE.ToUInt64(chunk, 2); int format; if (BitHelper.Read(ref x, 1) != 1) { return; } BitHelper.Read(ref x, 5); BitHelper.Read(ref x, 8); format = BitHelper.Read(ref x, 3); switch (format) { case 0: _width = BitHelper.Read(ref x, 8); _height = BitHelper.Read(ref x, 8); break; case 1: _width = BitHelper.Read(ref x, 16); _height = BitHelper.Read(ref x, 16); break; case 2: _width = 352; _height = 288; break; case 3: _width = 176; _height = 144; break; case 4: _width = 128; _height = 96; break; case 5: _width = 320; _height = 240; break; case 6: _width = 160; _height = 120; break; default: return; } } else if ((_codecID == 4) || (_codecID == 5)) { // Reference: vp6_parse_header from libavcodec's vp6.c int skip = (_codecID == 4) ? 1 : 4; if (chunk.Length < (skip + 8)) return; ulong x = BitConverterBE.ToUInt64(chunk, skip); int deltaFrameFlag = BitHelper.Read(ref x, 1); int quant = BitHelper.Read(ref x, 6); int separatedCoeffFlag = BitHelper.Read(ref x, 1); int subVersion = BitHelper.Read(ref x, 5); int filterHeader = BitHelper.Read(ref x, 2); int interlacedFlag = BitHelper.Read(ref x, 1); if (deltaFrameFlag != 0) { return; } if ((separatedCoeffFlag != 0) || (filterHeader == 0)) { BitHelper.Read(ref x, 16); } _height = BitHelper.Read(ref x, 8) * 16; _width = BitHelper.Read(ref x, 8) * 16; // chunk[0] contains the width and height (4 bits each, respectively) that should // be cropped off during playback, which will be non-zero if the encoder padded // the frames to a macroblock boundary. But if you use this adjusted size in the // AVI header, DirectShow seems to ignore it, and it can cause stride or chroma // alignment problems with VFW if the width/height aren't multiples of 4. if (!_isAlphaWriter) { int cropX = chunk[0] >> 4; int cropY = chunk[0] & 0x0F; if (((cropX != 0) || (cropY != 0)) && !_isAlphaWriter) { _warnings.Add(String.Format("Suggested cropping: {0} pixels from right, {1} pixels from bottom.", cropX, cropY)); } } } } private string CodecFourCC { get { if (_codecID == 2) { return "FLV1"; } if ((_codecID == 4) || (_codecID == 5)) { return "VP6F"; } return "NULL"; } } private void WriteIndexChunk() { uint indexDataSize = (uint)_frameCount * 16; WriteFourCC("idx1"); _bw.Write(indexDataSize); for (int i = 0; i < _frameCount; i++) { WriteFourCC("00dc"); _bw.Write(_index[(i * 3) + 0]); _bw.Write(_index[(i * 3) + 1]); _bw.Write(_index[(i * 3) + 2]); } _indexChunkSize = indexDataSize + 8; } public void Finish(FractionUInt32 averageFrameRate) { WriteIndexChunk(); _bw.BaseStream.Seek(4, SeekOrigin.Begin); _bw.Write((uint)(224 + _moviDataSize + _indexChunkSize - 8)); _bw.BaseStream.Seek(24 + 8, SeekOrigin.Begin); _bw.Write((uint)0); _bw.BaseStream.Seek(12, SeekOrigin.Current); _bw.Write((uint)_frameCount); _bw.BaseStream.Seek(12, SeekOrigin.Current); _bw.Write((uint)_width); _bw.Write((uint)_height); _bw.BaseStream.Seek(100 + 28, SeekOrigin.Begin); _bw.Write((uint)averageFrameRate.D); _bw.Write((uint)averageFrameRate.N); _bw.BaseStream.Seek(4, SeekOrigin.Current); _bw.Write((uint)_frameCount); _bw.BaseStream.Seek(16, SeekOrigin.Current); _bw.Write((ushort)_width); _bw.Write((ushort)_height); _bw.BaseStream.Seek(164 + 12, SeekOrigin.Begin); _bw.Write((uint)_width); _bw.Write((uint)_height); _bw.BaseStream.Seek(8, SeekOrigin.Current); _bw.Write((uint)(_width * _height * 6)); _bw.BaseStream.Seek(212 + 4, SeekOrigin.Begin); _bw.Write((uint)(_moviDataSize + 4)); _bw.Close(); if (_alphaWriter != null) { _alphaWriter.Finish(averageFrameRate); } } private void WriteFourCC(string fourCC) { byte[] bytes = System.Text.Encoding.ASCII.GetBytes(fourCC); if (bytes.Length != 4) { throw new Exception("Invalid FourCC length."); } _bw.Write(bytes); } public string Path { get { return _path; } } } class TimeCodeWriter { string _path; StreamWriter _sw; public TimeCodeWriter(string path) { _path = path; if (path != null) { _sw = new StreamWriter(path, false, Encoding.ASCII); _sw.WriteLine("# timecode format v2"); } } public void Write(uint timeStamp) { if (_sw != null) { _sw.WriteLine(timeStamp); } } public void Finish() { if (_sw != null) { _sw.Close(); _sw = null; } } public string Path { get { return _path; } } } public struct FractionUInt32 { public uint N; public uint D; public FractionUInt32(uint n, uint d) { N = n; D = d; } public double ToDouble() { return (double)N / (double)D; } public void Reduce() { uint gcd = GCD(N, D); N /= gcd; D /= gcd; } private uint GCD(uint a, uint b) { uint r; while (b != 0) { r = a % b; a = b; b = r; } return a; } public override string ToString() { return ToString(true); } public string ToString(bool full) { if (full) { return ToDouble().ToString() + " (" + N.ToString() + "/" + D.ToString() + ")"; } else { return ToDouble().ToString("0.####"); } } } }