From fd17a09426a6943e166b396da92f676263e342f3 Mon Sep 17 00:00:00 2001 From: Milek7 Date: Thu, 19 Jun 2014 22:48:19 +0200 Subject: [PATCH 1/5] changed line indents to Visual Studio style changed directory structure added Visual Studio 2013 project updated readme and .gitignore --- .gitattributes | 1 + .gitignore | 13 +- Download.cs | 496 ---- Mkv.cs | 2001 --------------- Mp4.cs | 359 --- Program.cs | 207 -- README.md | 9 +- Smooth.cs | 676 ----- Utils.cs | 132 - c.sh => compile-mono/compile.sh | 2 +- compile-visualstudio/App.config | 6 + .../Properties/AssemblyInfo.cs | 36 + compile-visualstudio/smoothget.csproj | 75 + compile-visualstudio/smoothget.sln | 22 + src/Download.cs | 596 +++++ src/Mkv.cs | 2286 +++++++++++++++++ src/Mp4.cs | 462 ++++ src/Program.cs | 261 ++ src/Smooth.cs | 829 ++++++ src/Utils.cs | 157 ++ 20 files changed, 4747 insertions(+), 3879 deletions(-) create mode 100644 .gitattributes delete mode 100644 Download.cs delete mode 100644 Mkv.cs delete mode 100644 Mp4.cs delete mode 100644 Program.cs delete mode 100644 Smooth.cs delete mode 100644 Utils.cs rename c.sh => compile-mono/compile.sh (83%) mode change 100755 => 100644 create mode 100644 compile-visualstudio/App.config create mode 100644 compile-visualstudio/Properties/AssemblyInfo.cs create mode 100644 compile-visualstudio/smoothget.csproj create mode 100644 compile-visualstudio/smoothget.sln create mode 100644 src/Download.cs create mode 100644 src/Mkv.cs create mode 100644 src/Mp4.cs create mode 100644 src/Program.cs create mode 100644 src/Smooth.cs create mode 100644 src/Utils.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9ac0ec3..f227130 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ -# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) -bin -obj +# Build folders +compile-mono/bin +compile-mono/obj +compile-visualstudio/bin +compile-visualstudio/obj -# mstest test results +# VS user-specific +compile-visualstudio/smoothget.v12.suo + +# mstest TestResults \ No newline at end of file diff --git a/Download.cs b/Download.cs deleted file mode 100644 index 4975802..0000000 --- a/Download.cs +++ /dev/null @@ -1,496 +0,0 @@ -using Smoothget; -using Smoothget.Mkv; -using Smoothget.Mp4; -using Smoothget.Smooth; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -namespace Smoothget.Download { - // Download progress for a single track. - // This is a pure data class, please don't add logic. - // Cannot use a `struct' here, because its fields are read-only once the constructor returns. - internal class Track { - public TrackInfo TrackInfo; - public ulong NextStartTime; // Start time of the next chunk to download. - public int DownloadedChunkCount; - public Track(TrackInfo trackInfo) { - this.TrackInfo = trackInfo; - this.DownloadedChunkCount = 0; - } - } - - // This is a pure data class, please don't add logic. - // Don't convert MediaSample to a struct, it makes the executable 512 bytes larger. - internal class MediaSample { - public long Offset; // In bytes. - public ulong StartTime; - public int Length; // In bytes. - public bool IsKeyFrame; - public MediaSample(long offset, int length, ulong startTime, bool isKeyFrame) { - this.Offset = offset; - this.Length = length; - this.StartTime = startTime; - this.IsKeyFrame = isKeyFrame; - } - } - - public interface IStoppable { - void Stop(); - } - - public delegate void SetupStop(bool isLive, IStoppable stoppable); - public delegate void DisplayDuration(ulong reachedTicks, ulong totalTicks); - - // To be passed to WriteMkv when isCombo == true. - internal class DownloadingMediaDataSource : IMediaDataSource, IStoppable { - // Usually Tracks has 2 elements: one for the video track and one for the audio track. - private IList Tracks; - private string ManifestParentPath; - private ulong MinStartTime; - private ulong TotalDuration; - private ulong TimeScale; - private DisplayDuration DisplayDuration; - - // Usually TrackSamples has 2 elements: one for the video track and one for the audio track. - private IList[] TrackSamples; - private int[] TrackSampleStartIndexes; - // this.TrackFirstBytes[i] corresponds to this.TrackSamples[i][this.TrackSampleStartIndexes[i]].GetBytes(), - // or null if not converted yet. - private MediaDataBlock[] TrackFirstBlocks; - // this.TrackFirstFileDatas[i] contains the whole file for this.TrackSamples[i][this.TrackSampleStartIndexes[i]] or - // it's null. - private byte[][] TrackFirstFileDatas; - private IChunkStartTimeReceiver ChunkStartTimeReceiver; - private bool IsLive; - public volatile bool IsStopped; - private ulong StopAfter; - private ulong TotalTicks; // For ETA calculation. - - // The object created may modify trackSamples in a destructive way, to save memory. - // Expects tracks[...].NextStartTime and tracks[...].DownloadedChunkCount to be initialized. - public DownloadingMediaDataSource(IList tracks, string manifestParentPath, - ulong timeScale, bool isLive, ulong stopAfter, ulong totalTicks, - DisplayDuration displayDuration) { - int trackCount = tracks.Count; - this.Tracks = tracks; - this.ManifestParentPath = manifestParentPath; - this.TimeScale = timeScale; - this.DisplayDuration = displayDuration; - this.IsLive = isLive; - this.StopAfter = stopAfter; - this.TotalDuration = 0; - this.MinStartTime = ulong.MaxValue; - this.IsStopped = false; - for (int i = 0; i < trackCount; ++i) { - ulong chunkStartTime = tracks[i].NextStartTime; - if (this.MinStartTime > chunkStartTime) { - this.MinStartTime = chunkStartTime; - } - } - this.TotalTicks = totalTicks; - this.TrackSamples = new IList[trackCount]; // Items initialized to null. - for (int i = 0; i < trackCount; ++i) { - this.TrackSamples[i] = new List(); - } - this.TrackSampleStartIndexes = new int[trackCount]; // Items initialized to 0. - this.TrackFirstBlocks = new MediaDataBlock[trackCount]; // Items initialized to null. - this.TrackFirstFileDatas = new byte[trackCount][]; // Items initialized to null. - this.ChunkStartTimeReceiver = null; - } - - /*implements IStoppable*/ public void Stop() { - this.IsStopped = true; // TODO: Is `volatile' enough to make this thread-safe? - } - - /*implements*/ public int GetTrackCount() { - return this.TrackFirstBlocks.Length; - } - - /*implements*/ public ulong GetTrackEndTime(int trackIndex) { - return this.Tracks[trackIndex].NextStartTime; - } - - /*implements*/ public void StartChunks(IChunkStartTimeReceiver chunkStartTimeReceiver) { - this.ChunkStartTimeReceiver = chunkStartTimeReceiver; - for (int trackIndex = 0; trackIndex < this.Tracks.Count; ++trackIndex) { - // Propagate the start time of the verify first chunk of the track. If resuming from a .muxstate, - // this also checks that the .muxstate is consistent with what we want to do. - // TODO: If not consistent, don't use the .muxstate instead of making the process abort. - this.ChunkStartTimeReceiver.SetChunkStartTime( - trackIndex, this.Tracks[trackIndex].DownloadedChunkCount, this.Tracks[trackIndex].NextStartTime); - } - } - - // Returns false on EOF, true on success. - private bool DownloadNextChunk(int trackIndex) { - IList mediaSamples = this.TrackSamples[trackIndex]; - Track track = this.Tracks[trackIndex]; - if ((track.DownloadedChunkCount >= track.TrackInfo.Stream.ChunkCount && !this.IsLive) || - this.IsStopped) return false; // EOF. - this.TrackSampleStartIndexes[trackIndex] = 0; - mediaSamples.Clear(); - while (mediaSamples.Count == 0) { // Download next chunk. - int chunkIndex = track.DownloadedChunkCount; - if ((chunkIndex >= track.TrackInfo.Stream.ChunkCount && !this.IsLive) || - this.IsStopped) return false; - ulong chunkStartTime = track.NextStartTime; - if (this.IsLive && chunkStartTime * 1e7 / this.TimeScale >= this.StopAfter) return false; - if (track.TrackInfo.Stream.ChunkList.Count > chunkIndex) { - ulong chunkStartTimeInList = track.TrackInfo.Stream.ChunkList[chunkIndex].StartTime; - if (chunkStartTime != chunkStartTimeInList) { - throw new Exception( - "StartTime mismatch in .ism and chunk files: ism=" + chunkStartTimeInList + - " file=" + chunkStartTime + " track=" + trackIndex + " chunk=" + chunkIndex); - } - } - byte[] contents = Downloader.DownloadChunk(track.TrackInfo, mediaSamples, chunkStartTime, this.ManifestParentPath, - this.IsLive, out track.NextStartTime); - if (contents == null) { - this.IsStopped = true; - // The URL has been printed by DownloadChunk above. - // TODO: Finish muxing the .mkv so the user gets something complete. - throw new Exception("Error downloading chunk " + chunkIndex + " of track " + trackIndex); - } - ++track.DownloadedChunkCount; - if (track.TrackInfo.Stream.ChunkList.Count > chunkIndex) { - ulong nextStartTimeInList = - chunkStartTime + track.TrackInfo.Stream.ChunkList[chunkIndex].Duration; - if (track.NextStartTime != nextStartTimeInList) { - throw new Exception( - "next StartTime mismatch in .ism and chunk files: ism=" + nextStartTimeInList + - " file=" + track.NextStartTime + " track=" + trackIndex + - " chunk=" + chunkIndex); - } - } - this.ChunkStartTimeReceiver.SetChunkStartTime( - trackIndex, track.DownloadedChunkCount, track.NextStartTime); - this.TrackFirstFileDatas[trackIndex] = contents; - // Notify the listener of the successful download. - ulong trackTotalDuration = track.NextStartTime - this.MinStartTime; - if (this.TotalDuration < trackTotalDuration) { - this.TotalDuration = trackTotalDuration; - ulong reachedTicks = (ulong)(this.TotalDuration * 1e7 / this.TimeScale); - this.DisplayDuration(reachedTicks, this.TotalTicks); - } - } - return true; - } - - /*implements*/ public MediaDataBlock PeekBlock(int trackIndex) { - if (this.TrackFirstBlocks[trackIndex] != null) return this.TrackFirstBlocks[trackIndex]; - IList mediaSamples = this.TrackSamples[trackIndex]; - int k = this.TrackSampleStartIndexes[trackIndex]; - if (k >= mediaSamples.Count) { // Finished processing this chunk, download next chunk. - if (!DownloadNextChunk(trackIndex)) return null; - k = 0; - } - - MediaSample mediaSample = mediaSamples[k]; - mediaSamples[k] = null; // Save memory once this function returns. - return this.TrackFirstBlocks[trackIndex] = new MediaDataBlock( - new ArraySegment(this.TrackFirstFileDatas[trackIndex], (int)mediaSample.Offset, mediaSample.Length), - mediaSample.StartTime, mediaSample.IsKeyFrame); - } - - /*implements*/ public void ConsumeBlock(int trackIndex) { - if (this.TrackFirstBlocks[trackIndex] == null && this.PeekBlock(trackIndex) == null) { - throw new Exception("ASSERT: No MediaSample to consume."); - } - this.TrackFirstBlocks[trackIndex] = null; // Save memory and signify for the next call to PeekBlock. - if (++this.TrackSampleStartIndexes[trackIndex] >= this.TrackSamples[trackIndex].Count) { - this.TrackSamples[trackIndex].Clear(); // Save memory. - this.TrackFirstFileDatas[trackIndex] = null; // Save memory. - } - } - - /*implements:*/ public void ConsumeBlocksUntil(int trackIndex, ulong startTime) { - // This is correct for ETA only if ConsumeBlocksUntil is called once. - int k = this.TrackSampleStartIndexes[trackIndex]; - IList mediaSamples = this.TrackSamples[trackIndex]; - int mediaSampleCount = mediaSamples.Count; - if (this.TrackFirstBlocks[trackIndex] != null) { - // TODO: Test this branch. - if (this.TrackFirstBlocks[trackIndex].StartTime > startTime) return; // Nothing to consume. - this.TrackFirstBlocks[trackIndex] = null; // Save memory and signify for the next call to PeekBlock. - if (k < mediaSampleCount && mediaSamples[k].StartTime > startTime) { - throw new Exception("ASSERT: Inconsistent TrackFirstBlocks and TrackSamples."); - } - } - Track track = this.Tracks[trackIndex]; - // GetChunkStartTime returns ulong.MaxValue if the chunk index is too large for it. Good. It shouldn't happen - // though, because the next start time after track.DownloadedChunkCount is always available. - if (k < mediaSampleCount && - this.ChunkStartTimeReceiver.GetChunkStartTime(trackIndex, track.DownloadedChunkCount) > startTime) { - // We may find where to stop within the current chunk (mediaSamples). - // TODO: Test this branch. - for (; k < mediaSampleCount; ++k) { - if (mediaSamples[k].StartTime > startTime) { - this.TrackSampleStartIndexes[trackIndex] = k; - return; - } - } - } - // Consumed the whole mediaSamples. - mediaSamples.Clear(); // Save memory. - this.TrackFirstFileDatas[trackIndex] = null; // Save memory. - this.TrackSampleStartIndexes[trackIndex] = 0; - - // Consume chunks which start too early. This step makes resuming downloads very fast, because there are whole - // chunk files which we don't have to download again. - // - // GetChunkStartTime returns ulong.MaxValue if the chunk index is too large for it. Good. - // - // We could use track.TrackInfo.Stream.ChunkList here to consume more in this loop, but by design we don't rely - // on ChunkList. Using ChunkList here wouldn't make resuming previous downloads (using .muxstate) faster, - // because GetChunkStartTime provides the necessary speedup for that. - // - // TODO: Do a binary search. (The effect of this optimization would be negligible.) - ulong nextNextChunkStartTime; - while ((nextNextChunkStartTime = this.ChunkStartTimeReceiver.GetChunkStartTime( - trackIndex, track.DownloadedChunkCount + 1)) <= startTime) { - // Consume chunk with index track.DownloadedChunkCount. - track.NextStartTime = nextNextChunkStartTime; - ++track.DownloadedChunkCount; - } - - // At this point the next chunk (track.DownloadedChunkCount) starts <= startTime, and the chunk after that - // (track.DownloadedChunkCount + 1) doesn't exist or starts > startTime. So we load the next chunk, and consume - // every mediaSample starting <= startTime in it. That's enough, because there is nothing to consume in further - // chunks (starting with the ``after that'' chunk). We have a `while' loop in case GetChunkStartTime above - // has returned ulong.MaxValue, so we have do download more in order to figure out what to consume. - while (DownloadNextChunk(trackIndex)) { - mediaSampleCount = mediaSamples.Count; - if (mediaSampleCount == 0) { - throw new Exception("ASSERT: Expected media samples after download."); - } - for (k = 0; k < mediaSampleCount; ++k) { - if (mediaSamples[k].StartTime > startTime) { - this.TrackSampleStartIndexes[trackIndex] = k; - return; - } - } - mediaSamples.Clear(); // Save memory. - } - // Just reached EOF on trackIndex. - this.TrackSamples[trackIndex].Clear(); // Save memory. - this.TrackFirstFileDatas[trackIndex] = null; // Save memory. - } - } - - // TODO: Move most of Downloader outside Downloader. - public class Downloader { - // Exactly one of manifestUri and manifestPath must be set. - public static void DownloadAndMux(Uri manifestUri, string manifestPath, string mkvPath, bool isDeterministic, TimeSpan stopAfter, - SetupStop setupStop, DisplayDuration displayDuration) { - string manifestParentPath = null; // A null indicates a remote manifest file. - ManifestInfo manifestInfo; - if (manifestPath != null) { - manifestParentPath = Path.GetDirectoryName(manifestPath); - Console.WriteLine("Parsing local manifest file: " + manifestPath); - using (FileStream manifestStream = new FileStream(manifestPath, FileMode.Open)) { - manifestInfo = ManifestInfo.ParseManifest(manifestStream, /*manifestUri:*/new Uri(LOCAL_URL_PREFIX)); - } - } else { - Console.WriteLine("Downloading and parsing manifest: " + manifestUri); - WebClient webClient = new WebClient(); - using (Stream manifestStream = webClient.OpenRead(manifestUri)) { - manifestInfo = ManifestInfo.ParseManifest(manifestStream, manifestUri); - } - } - Console.Write(manifestInfo.GetDescription()); - - IList tracks = new List(); - foreach (StreamInfo streamInfo in manifestInfo.SelectedStreams) { - foreach (TrackInfo trackInfo in streamInfo.SelectedTracks) { - tracks.Add(new Track(trackInfo)); - } - } - IList trackEntries = new List(); - IList> trackSamples = new List>(); - for (int i = 0; i < tracks.Count; ++i) { - trackEntries.Add(tracks[i].TrackInfo.TrackEntry); - trackEntries[i].TrackNumber = (ulong)(i + 1); - trackSamples.Add(new List()); - } - for (int i = 0; i < tracks.Count; i++) { - // TODO: Add a facility to start live streams from a later chunk (it was chunkIndex=10 previously). - // Our design allows for an empty ChunkList, in case live streams are growing. - tracks[i].NextStartTime = tracks[i].TrackInfo.Stream.ChunkList.Count == 0 ? 0 : - tracks[i].TrackInfo.Stream.ChunkList[0].StartTime; - } - // TODO: Test for live streams (see the StackOverflow question). - Console.WriteLine("Also muxing selected tracks to MKV: " + mkvPath); - try { - if (Directory.GetParent(mkvPath) != null && - !Directory.GetParent(mkvPath).Exists) - Directory.GetParent(mkvPath).Create(); - } catch (IOException) { - // TODO: Add nicer error reporting, without a stack trace. - throw new Exception("Cannot not create the directory of .mkv: " + mkvPath); - } - ulong maxTrackEndTimeHint = manifestInfo.Duration; - for (int i = 0; i < tracks.Count; ++i) { - IList chunkInfos = tracks[i].TrackInfo.Stream.ChunkList; - int j = chunkInfos.Count - 1; - if (j >= 0) { // Our design allows for an empty ChunkList. - ulong trackDuration = chunkInfos[j].StartTime + chunkInfos[j].Duration; - if (maxTrackEndTimeHint < trackDuration) maxTrackEndTimeHint = trackDuration; - } - } - // The .muxstate file is approximately 1/5441.43 of the size of the .mkv. - // The .muxstate file is around 28.088 bytes per second. TODO: Update this after n. - // Sometimes totalDuration of video is 1156420602, audio is 1156818141 (larger), so we just take the maximum. - string muxStatePath = Path.ChangeExtension(mkvPath, "muxstate"); - string muxStateOldPath = muxStatePath + ".old"; - byte[] oldMuxState = null; - if (File.Exists(muxStatePath)) { // False for directories. - using (FileStream fileStream = new FileStream(muxStatePath, FileMode.Open)) { - oldMuxState = ReadFileStream(fileStream); - } - if (oldMuxState.Length > 0) { - // File.Move fails with IOException if the destination already exists. - // C# and .NET SUXX: There is no atomic overwrite-move. - try { - File.Move(muxStatePath, muxStateOldPath); - } catch (IOException) { - File.Replace(muxStatePath, muxStateOldPath, null, true); - } - } - } - DownloadingMediaDataSource source = new DownloadingMediaDataSource( - tracks, manifestParentPath, manifestInfo.TimeScale, - manifestInfo.IsLive, (ulong)stopAfter.Ticks, manifestInfo.TotalTicks, displayDuration); - setupStop(manifestInfo.IsLive, source); - MuxStateWriter muxStateWriter = new MuxStateWriter(new FileStream(muxStatePath, FileMode.Create)); - try { - MkvUtils.WriteMkv(mkvPath, trackEntries, source, maxTrackEndTimeHint, manifestInfo.TimeScale, isDeterministic, - oldMuxState, muxStateWriter); - } finally { - muxStateWriter.Close(); - } - File.Delete(muxStatePath); - if (File.Exists(muxStateOldPath)) { - File.Delete(muxStateOldPath); - } - } - - private static readonly string LOCAL_URL_PREFIX = "http://local/"; - - // Modifies track in place, and appends to mediaSamples. - // Returns null on network failure or empty file, otherwise it returns a non-empty array. - // The chunk file contents are returned, and are not saved to disk. - internal static byte[] DownloadChunk(TrackInfo trackInfo, IList mediaSamples, ulong chunkStartTime, - string manifestParentPath, bool isLive, out ulong nextStartTime) { - nextStartTime = 0; // Set even if null is returned. - string chunkUrl = trackInfo.Stream.GetChunkUrl(trackInfo.Bitrate, chunkStartTime); - // TODO: Move TrackInfo away from Track, keep only fields necessary here, excluding ChunkList. - byte[] downloadedBytes; // Will be set below. - if (manifestParentPath != null) { // It was a local manifest, so read the chunk from a local file. - if (!chunkUrl.StartsWith(LOCAL_URL_PREFIX)) { - throw new Exception("ASSERT: Missing local URL prefix."); - } - // Example chunk URL: "http://local/QualityLevels(900000)/Fragments(video=0)". - // TODO: Maybe this needs some further unescaping of %5A etc. (can be tested locally). - string chunkDownloadedPath = manifestParentPath + Path.DirectorySeparatorChar + - chunkUrl.Substring(LOCAL_URL_PREFIX.Length).Replace('/', Path.DirectorySeparatorChar); - using (FileStream fileStream = new FileStream(chunkDownloadedPath, FileMode.Open)) { - downloadedBytes = ReadFileStream(fileStream); - } - if (downloadedBytes.Length == 0) { - Console.WriteLine(); - Console.WriteLine("Local chunk file empty: " + chunkDownloadedPath); - return null; - } - } else { // Download from the web. - WebClient webClient = new WebClient(); - try { - // TODO: What's the timeout on this? - downloadedBytes = webClient.DownloadData(chunkUrl); - } catch (WebException) { - Thread.Sleep(isLive ? 4000 : 2000); - try { - downloadedBytes = webClient.DownloadData(chunkUrl); - } catch (WebException) { - Thread.Sleep(isLive ? 6000 : 3000); - try { - downloadedBytes = webClient.DownloadData(chunkUrl); - } catch (WebException) { - // It's an acceptable behavior to stop downloading live streams after 10 seconds. - // If it's really live, there should be a new chunk update available every 10 seconds. - Console.WriteLine(); - Console.WriteLine("Error downloading chunk " + chunkUrl); - return null; - } - } - } - } - if (downloadedBytes.Length == 0) { - Console.WriteLine(); - Console.WriteLine("Chunk empty: " + chunkUrl); - return null; - } - Fragment fragment = new Fragment(downloadedBytes, 0, downloadedBytes.Length); - // This appends to mediaSamples. - nextStartTime = ParseFragment(fragment, mediaSamples, trackInfo.Stream.Type, chunkStartTime); - if (nextStartTime <= chunkStartTime) { - throw new Exception("Found empty chunk."); - } - return downloadedBytes; - } - - // TODO: Move this to a generic utility class. - private static byte[] ReadFileStream(FileStream fileStream) { - int fileSize = (int)fileStream.Length; // TODO: Can this be negative etc. for non-regular files? - if (fileSize <= 0) return new byte[0]; - byte[] array = new byte[fileSize]; - if (fileSize != fileStream.Read(array, 0, fileSize)) { - throw new Exception("ASSERT: Short read from MediaSample file " + fileStream.Name + ", wanted " + fileSize); - } - if (0 != fileStream.Read(array, 0, 1)) { - throw new Exception("ASSERT: Long read from MediaSample file " + fileStream.Name + ", wanted " + fileSize); - } - return array; - } - - // Appends to `samples'. - // Returns nextStartTime. - private static ulong ParseFragment(Fragment fragment, IList samples, MediaStreamType type, - ulong chunkStartTime) { - // A fragment is a ``chunk'' (with a corresponding in its duration) in the ISM manifest file. - TrackFragmentBox traf = fragment.moof.traf; - if (traf.tfxd != null) { - chunkStartTime = traf.tfxd.FragmentAbsoluteTime; - } - ulong nextStartTime = 0uL; - if (traf.tfrf != null && traf.tfrf.Array.Length > 0u) { - nextStartTime = traf.tfrf.Array[0].FragmentAbsoluteTime; - } - long sampleOffset = fragment.mdat.Start; - uint defaultSampleSize = traf.tfhd.default_sample_size; - uint sampleSize = defaultSampleSize; - uint defaultSampleDuration = traf.tfhd.default_sample_duration; - uint duration = defaultSampleDuration; - ulong totalDuration = 0; - uint sampleCount = traf.trun.sample_count; - TrackRunBox.Element[] array = defaultSampleSize == 0u || defaultSampleDuration == 0u ? traf.trun.array : null; - for (uint i = 0; i < sampleCount; ++i) { - if (defaultSampleSize == 0u) { - sampleSize = array[i].sample_size; - } - if (defaultSampleDuration == 0u) { - duration = array[i].sample_duration; - } - // We add a few dozen MediaSample entries for a chunk. - samples.Add(new MediaSample(sampleOffset, (int)sampleSize, chunkStartTime, - /*isKeyFrame:*/i == 0 || type == MediaStreamType.Audio)); - chunkStartTime += (ulong)duration; - totalDuration += (ulong)duration; - sampleOffset += sampleSize; - } - return nextStartTime != 0uL ? nextStartTime : chunkStartTime; - } - } -} diff --git a/Mkv.cs b/Mkv.cs deleted file mode 100644 index 2dd2797..0000000 --- a/Mkv.cs +++ /dev/null @@ -1,2001 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -namespace Smoothget.Mkv { - public enum CodecID { - V_AVC, - V_MS, - A_AAC, - A_MS - } - - // Integer values correspond to the return values of GetVIntForTrackType. - public enum TrackType { - Video = 1, - Audio = 2, - Complex = 3, - Logo = 16, - Subtitle = 17, - Buttons = 18, - Control = 32, - } - - public struct CuePoint { - private ulong CueTime; - private ulong CueTrack; - public ulong CueClusterPosition; - public CuePoint(ulong cueTime, ulong cueTrack, ulong cueClusterPosition) { - this.CueTime = cueTime; - this.CueTrack = cueTrack; - this.CueClusterPosition = cueClusterPosition; - } - public byte[] GetBytes() { - // TODO: Do this with fewer temporary arrays. - return MkvUtils.GetEEBytes(ID.CuePoint, Utils.CombineBytes( - MkvUtils.GetEEBytes(ID.CueTime, MkvUtils.GetVintBytes(this.CueTime)), - MkvUtils.GetEEBytes(ID.CueTrackPositions, Utils.CombineBytes( - MkvUtils.GetEEBytes(ID.CueTrack, MkvUtils.GetVintBytes(this.CueTrack)), - MkvUtils.GetEEBytes(ID.CueClusterPosition, MkvUtils.GetVintBytes(this.CueClusterPosition)))))); - } - } - - // Integer values correspond to GetBytesForID: MkvUtils.GetDataSizeBytes((ulong)id). - public enum ID { - EBML = 172351395, - EBMLVersion = 646, - EBMLReadVersion = 759, - EBMLMaxIDLength = 754, - EBMLMaxSizeLength = 755, - DocType = 642, - DocTypeVersion = 647, - DocTypeReadVersion = 645, - Void = 108, - Segment = 139690087, - SeekHead = 21863284, - Seek = 3515, - SeekID = 5035, - SeekPosition = 5036, - Info = 88713574, - SegmentUID = 13220, - TimecodeScale = 710577, - Duration = 1161, - DateUTC = 1121, - MuxingApp = 3456, - WritingApp = 5953, - Cluster = 256095861, - Timecode = 103, - SimpleBlock = 35, - Tracks = 106212971, - TrackEntry = 46, - TrackNumber = 87, - TrackUID = 13253, - TrackType = 3, - FlagEnabled = 57, - FlagDefault = 8, - FlagForced = 5546, - FlagLacing = 28, - Name = 4974, - Language = 177564, - CodecID = 6, - CodecPrivate = 9122, - Video = 96, - FlagInterlaced = 26, - PixelWidth = 48, - PixelHeight = 58, - DisplayWidth = 5296, - DisplayHeight = 5306, - Audio = 97, - SamplingFrequency = 53, - Channels = 31, - BitDepth = 8804, - Cues = 206814059, - CuePoint = 59, - CueTime = 51, - CueTrackPositions = 55, - CueTrack = 119, - CueClusterPosition = 113, - } - - // See also TrackEntry.LanguageCodes. - // Please don't change the order of the names here, because TrackEntry.LanguageCodes corresponds to it. - public enum LanguageID { - Abkhazian, - Achinese, - Acoli, - Adangme, - Adygei, - Adyghe, - Afar, - Afrihili, - Afrikaans, - AfroAsiaticLanguages, - Ainu, - Akan, - Akkadian, - Albanian, - Alemannic, - Aleut, - AlgonquianLanguages, - Alsatian, - AltaicLanguages, - Amharic, - Angika, - ApacheLanguages, - Arabic, - Aragonese, - Arapaho, - Arawak, - Armenian, - Aromanian, - ArtificialLanguages, - Arumanian, - Assamese, - Asturian, - Asturleonese, - AthapascanLanguages, - AustralianLanguages, - AustronesianLanguages, - Avaric, - Avestan, - Awadhi, - Aymara, - Azerbaijani, - Bable, - Balinese, - BalticLanguages, - Baluchi, - Bambara, - BamilekeLanguages, - BandaLanguages, - BantuLanguages, - Basa, - Bashkir, - Basque, - BatakLanguages, - Bedawiyet, - Beja, - Belarusian, - Bemba, - Bengali, - BerberLanguages, - Bhojpuri, - BihariLanguages, - Bikol, - Bilin, - Bini, - Bislama, - Blin, - Bliss, - Blissymbolics, - Blissymbols, - BokmålNorwegian, - Bosnian, - Braj, - Breton, - Buginese, - Bulgarian, - Buriat, - Burmese, - Caddo, - Castilian, - Catalan, - CaucasianLanguages, - Cebuano, - CelticLanguages, - CentralAmericanIndianLanguages, - CentralKhmer, - Chagatai, - ChamicLanguages, - Chamorro, - Chechen, - Cherokee, - Chewa, - Cheyenne, - Chibcha, - Chichewa, - Chinese, - Chinookjargon, - Chipewyan, - Choctaw, - Chuang, - ChurchSlavic, - ChurchSlavonic, - Chuukese, - Chuvash, - ClassicalNepalBhasa, - ClassicalNewari, - ClassicalSyriac, - CookIslandsMaori, - Coptic, - Cornish, - Corsican, - Cree, - Creek, - CreolesAndPidgins, - CreolesAndPidginsEnglishBased, - CreolesAndPidginsFrenchBased, - CreolesAndPidginsPortugueseBased, - CrimeanTatar, - CrimeanTurkish, - Croatian, - CushiticLanguages, - Czech, - Dakota, - Danish, - Dargwa, - Delaware, - DeneSuline, - Dhivehi, - Dimili, - Dimli, - Dinka, - Divehi, - Dogri, - Dogrib, - DravidianLanguages, - Duala, - Dutch, - DutchMiddle, - Dyula, - Dzongkha, - EasternFrisian, - Edo, - Efik, - Egyptian, - Ekajuk, - Elamite, - English, - EnglishMiddle, - EnglishOld, - Erzya, - Esperanto, - Estonian, - Ewe, - Ewondo, - Fang, - Fanti, - Faroese, - Fijian, - Filipino, - Finnish, - FinnoUgrianLanguages, - Flemish, - Fon, - French, - FrenchMiddle, - FrenchOld, - Friulian, - Fulah, - Ga, - Gaelic, - GalibiCarib, - Galician, - Ganda, - Gayo, - Gbaya, - Geez, - Georgian, - German, - GermanLow, - GermanMiddleHigh, - GermanOldHigh, - GermanicLanguages, - Gikuyu, - Gilbertese, - Gondi, - Gorontalo, - Gothic, - Grebo, - GreekAncient, - GreekModern, - Greenlandic, - Guarani, - Gujarati, - Gwichin, - Haida, - Haitian, - HaitianCreole, - Hausa, - Hawaiian, - Hebrew, - Herero, - Hiligaynon, - HimachaliLanguages, - Hindi, - HiriMotu, - Hittite, - Hmong, - Hungarian, - Hupa, - Iban, - Icelandic, - Ido, - Igbo, - IjoLanguages, - Iloko, - ImperialAramaic, - InariSami, - IndicLanguages, - IndoEuropeanLanguages, - Indonesian, - Ingush, - Interlingua, - Interlingue, - Inuktitut, - Inupiaq, - IranianLanguages, - Irish, - IrishMiddle, - IrishOld, - IroquoianLanguages, - Italian, - Japanese, - Javanese, - Jingpho, - JudeoArabic, - JudeoPersian, - Kabardian, - Kabyle, - Kachin, - Kalaallisut, - Kalmyk, - Kamba, - Kannada, - Kanuri, - Kapampangan, - KaraKalpak, - KarachayBalkar, - Karelian, - KarenLanguages, - Kashmiri, - Kashubian, - Kawi, - Kazakh, - Khasi, - KhoisanLanguages, - Khotanese, - Kikuyu, - Kimbundu, - Kinyarwanda, - Kirdki, - Kirghiz, - Kirmanjki, - Klingon, - Komi, - Kongo, - Konkani, - Korean, - Kosraean, - Kpelle, - KruLanguages, - Kuanyama, - Kumyk, - Kurdish, - Kurukh, - Kutenai, - Kwanyama, - Kyrgyz, - Ladino, - Lahnda, - Lamba, - LandDayakLanguages, - Lao, - Latin, - Latvian, - Leonese, - Letzeburgesch, - Lezghian, - Limburgan, - Limburger, - Limburgish, - Lingala, - Lithuanian, - Lojban, - LowGerman, - LowSaxon, - LowerSorbian, - Lozi, - LubaKatanga, - LubaLulua, - Luiseno, - LuleSami, - Lunda, - Luo, - Lushai, - Luxembourgish, - MacedoRomanian, - Macedonian, - Madurese, - Magahi, - Maithili, - Makasar, - Malagasy, - Malay, - Malayalam, - Maldivian, - Maltese, - Manchu, - Mandar, - Mandingo, - Manipuri, - ManoboLanguages, - Manx, - Maori, - Mapuche, - Mapudungun, - Marathi, - Mari, - Marshallese, - Marwari, - Masai, - MayanLanguages, - Mende, - Mikmaq, - Micmac, - Minangkabau, - Mirandese, - Mohawk, - Moksha, - Moldavian, - Moldovan, - MonKhmerLanguages, - Mong, - Mongo, - Mongolian, - Mossi, - MultipleLanguages, - MundaLanguages, - NKo, - NahuatlLanguages, - Nauru, - Navaho, - Navajo, - NdebeleNorth, - NdebeleSouth, - Ndonga, - Neapolitan, - NepalBhasa, - Nepali, - Newari, - Nias, - NigerKordofanianLanguages, - NiloSaharanLanguages, - Niuean, - Nolinguisticcontent, - Nogai, - NorseOld, - NorthAmericanIndianLanguages, - NorthNdebele, - NorthernFrisian, - NorthernSami, - NorthernSotho, - Norwegian, - NorwegianBokmål, - NorwegianNynorsk, - Notapplicable, - NubianLanguages, - Nuosu, - Nyamwezi, - Nyanja, - Nyankole, - NynorskNorwegian, - Nyoro, - Nzima, - Occidental, - Occitan, - OccitanOld, - OfficialAramaic, - Oirat, - Ojibwa, - OldBulgarian, - OldChurchSlavonic, - OldNewari, - OldSlavonic, - Oriya, - Oromo, - Osage, - Ossetian, - Ossetic, - OtomianLanguages, - Pahlavi, - Palauan, - Pali, - Pampanga, - Pangasinan, - Panjabi, - Papiamento, - PapuanLanguages, - Pashto, - Pedi, - Persian, - PersianOld, - PhilippineLanguages, - Phoenician, - Pilipino, - Pohnpeian, - Polish, - Portuguese, - PrakritLanguages, - ProvençalOld, - Punjabi, - Pushto, - Quechua, - Rajasthani, - Rapanui, - Rarotongan, - ReservedForLocalUse, - RomanceLanguages, - Romanian, - Romansh, - Romany, - Rundi, - Russian, - Sakan, - SalishanLanguages, - SamaritanAramaic, - SamiLanguages, - Samoan, - Sandawe, - Sango, - Sanskrit, - Santali, - Sardinian, - Sasak, - SaxonLow, - Scots, - ScottishGaelic, - Selkup, - SemiticLanguages, - Sepedi, - Serbian, - Serer, - Shan, - Shona, - SichuanYi, - Sicilian, - Sidamo, - SignLanguages, - Siksika, - Sindhi, - Sinhala, - Sinhalese, - SinoTibetanLanguages, - SiouanLanguages, - SkoltSami, - Slave, - SlavicLanguages, - Slovak, - Slovenian, - Sogdian, - Somali, - SonghaiLanguages, - Soninke, - SorbianLanguages, - SothoNorthern, - SothoSouthern, - SouthAmericanIndianLanguages, - SouthNdebele, - SouthernAltai, - SouthernSami, - Spanish, - SrananTongo, - Sukuma, - Sumerian, - Sundanese, - Susu, - Swahili, - Swati, - Swedish, - SwissGerman, - Syriac, - Tagalog, - Tahitian, - TaiLanguages, - Tajik, - Tamashek, - Tamil, - Tatar, - Telugu, - Tereno, - Tetum, - Thai, - Tibetan, - Tigre, - Tigrinya, - Timne, - Tiv, - tlhInganHol, - Tlingit, - TokPisin, - Tokelau, - TongaNyasa, - TongaTongaIslands, - Tsimshian, - Tsonga, - Tswana, - Tumbuka, - TupiLanguages, - Turkish, - TurkishOttoman, - Turkmen, - Tuvalu, - Tuvinian, - Twi, - Udmurt, - Ugaritic, - Uighur, - Ukrainian, - Umbundu, - UncodedLanguages, - Undetermined, - UpperSorbian, - Urdu, - Uyghur, - Uzbek, - Vai, - Valencian, - Venda, - Vietnamese, - Volapük, - Votic, - WakashanLanguages, - Walloon, - Waray, - Washo, - Welsh, - WesternFrisian, - WesternPahariLanguages, - Wolaitta, - Wolaytta, - Wolof, - Xhosa, - Yakut, - Yao, - Yapese, - Yiddish, - Yoruba, - YupikLanguages, - ZandeLanguages, - Zapotec, - Zaza, - Zazaki, - Zenaga, - Zhuang, - Zulu, - Zuni - } - - public class TrackEntry { - // `private const string' would increase the .exe size by 5 kB here. - private static readonly string LanguageCodes = "abkaceachadaadyadyaarafhafrafaainakaakkalbgswalealggswtutamhanpapaaraargarparwarmrupartrupasmastastathausmapavaaveawaaymazeastbanbatbalbambaibadbntbasbakbaqbtkbejbejbelbembenberbhobihbikbynbinbisbynzblzblzblnobbosbrabrebugbulbuaburcadspacatcaucebcelcaikhmchgcmcchachechrnyachychbnyachichnchpchozhachuchuchkchvnwcnwcsycrarcopcorcoscremuscrpcpecpfcppcrhcrhhrvcusczedakdandardelchpdivzzazzadindivdoidgrdraduadutdumdyudzofrsbinefiegyekaelxengenmangmyvepoesteweewofanfatfaofijfilfinfiudutfonfrefrmfrofurfulgaaglacarglgluggaygbagezgeogerndsgmhgohgemkikgilgongorgotgrbgrcgrekalgrngujgwihaihathathauhawhebherhilhimhinhmohithmnhunhupibaiceidoiboijoiloarcsmnincineindinhinaileikuipkiraglemgasgairoitajpnjavkacjrbjprkbdkabkackalxalkamkankaupamkaakrckrlkarkascsbkawkazkhakhikhokikkmbkinzzakirzzatlhkomkonkokkorkoskpekrokuakumkurkrukutkuakirladlahlamdaylaolatlavastltzlezlimlimlimlinlitjbondsndsdsblozlublualuismjlunluolusltzrupmacmadmagmaimakmlgmaymaldivmltmncmdrmanmnimnoglvmaoarnarnmarchmmahmwrmasmynmenmicmicminmwlmohmdfrumrummkhhmnlolmonmosmulmunnqonahnaunavnavndenblndonapnewnepnewnianicssaniuzxxnognonnaindefrrsmensonornobnnozxxnubiiinymnyanynnnonyonziileociproarcxalojichuchunwcchuoriormosaossossotopalpauplipampagpanpappaapusnsoperpeophiphnfilponpolporprapropanpusquerajraprarqaaroarumrohromrunruskhosalsamsmismosadsagsansatsrdsasndsscoglaselsemnsosrpsrrshnsnaiiiscnsidsgnblasndsinsinsitsiosmsdenslasloslvsogsomsonsnkwennsosotsainblaltsmaspasrnsuksuxsunsusswasswswegswsyrtgltahtaitgktmhtamtatteltertetthatibtigtirtemtivtlhtlitpitkltogtontsitsotsntumtupturotatuktvltyvtwiudmugauigukrumbmisundhsburduiguzbvaicatvenvievolvotwakwlnwarwaswelfryhimwalwalwolxhosahyaoyapyidyorypkzndzapzzazzazenzhazulzun"; - private static string GetLanguageCode(LanguageID id) { - int i = (int)id; - int i3 = i * 3; - if (i < 0 || i3 >= LanguageCodes.Length) { - throw new Exception(string.Format("LanguageID '{0}' is unsupported!", id)); - } - return LanguageCodes.Substring(i3, 3); - } - - // TODO: Change public to private. - public ulong TrackNumber; - public TrackType TrackType; - public string Name; - public LanguageID Language = LanguageID.English; - public CodecID CodecID; - private byte[] CodecPrivate; - private byte[] InfoBytes; - public TrackEntry(TrackType trackType, byte[] infoBytes, CodecID codecID, byte[] codecPrivate) { - this.TrackType = trackType; - this.InfoBytes = infoBytes; - this.CodecID = codecID; - this.CodecPrivate = codecPrivate; - } - public byte[] GetBytes() { - if (this.TrackNumber == 0uL) { - throw new Exception("TrackNumber must be greater than 0!"); - } - ulong trackUID = this.TrackNumber; - if (trackUID == 0uL) { - throw new Exception("TrackUID must be greater than 0!"); - } - List list = new List(); - list.Add(MkvUtils.GetEEBytes(ID.TrackNumber, MkvUtils.GetVintBytes(this.TrackNumber))); - list.Add(MkvUtils.GetEEBytes(ID.TrackUID, MkvUtils.GetVintBytes(trackUID))); - list.Add(MkvUtils.GetEEBytes(ID.TrackType, MkvUtils.GetVintBytes((ulong)this.TrackType))); - list.Add(MkvUtils.GetEEBytes(ID.FlagEnabled, MkvUtils.GetVIntForFlag(true))); - list.Add(MkvUtils.GetEEBytes(ID.FlagDefault, MkvUtils.GetVIntForFlag(true))); - list.Add(MkvUtils.GetEEBytes(ID.FlagForced, MkvUtils.GetVIntForFlag(false))); - list.Add(MkvUtils.GetEEBytes(ID.FlagLacing, MkvUtils.GetVIntForFlag(true))); - if (!string.IsNullOrEmpty(this.Name)) { - list.Add(MkvUtils.GetEEBytes(ID.Name, Encoding.UTF8.GetBytes(this.Name))); - } - if (this.Language != LanguageID.English) { - list.Add(MkvUtils.GetEEBytes(ID.Language, Encoding.ASCII.GetBytes(GetLanguageCode(this.Language)))); - } - list.Add(MkvUtils.GetEEBytes(ID.CodecID, Encoding.ASCII.GetBytes(MkvUtils.GetStringForCodecID(this.CodecID)))); - if (this.CodecPrivate != null) { - list.Add(MkvUtils.GetEEBytes(ID.CodecPrivate, this.CodecPrivate)); - } - list.Add(this.InfoBytes); - return MkvUtils.GetEEBytes(ID.TrackEntry, Utils.CombineByteArrays(list)); - } - } - - // This is a pure data class, please don't add logic. - public class MediaDataBlock { - // The media sample data bytes (i.e. a frame from the media file). - public ArraySegment Bytes; - public ulong StartTime; - public bool IsKeyFrame; - - public MediaDataBlock(ArraySegment bytes, ulong startTime, bool isKeyFrame) { - this.Bytes = bytes; - this.StartTime = startTime; - this.IsKeyFrame = isKeyFrame; - } - } - - public interface IChunkStartTimeReceiver { - // Returns ulong.MaxValue if the information is not available (i.e. chunkIndex is too large). - ulong GetChunkStartTime(int trackIndex, int chunkIndex); - void SetChunkStartTime(int trackIndex, int chunkIndex, ulong chunkStartTime); - } - - // A source (forward-iterator) of MediaDataBlock objects in a fixed number of parallel tracks. - public interface IMediaDataSource { - int GetTrackCount(); - // Called only when all chunks have been read (using ConsumeBlock). - ulong GetTrackEndTime(int trackIndex); - void StartChunks(IChunkStartTimeReceiver chunkStartTimeReceiver); - // Returns the first MediaDataBlock (i.e. with smallest StartTime) unconsumed MediaDataBlock on the specified track, or - // null on EOF on the specified track. The ownership of the returned - // block is shared between the MediaDataSource and the caller until ConsumeBlock(trackIndex) is called. Afterwards the - // the MediaDataSource releases ownership. The MediaDataSource never modifies the fields of the MediaDataBlock or the - // contents of its .Bytes array it returns, and - // it returns the same reference until ConsumeSample(trackIndex) is called, and a different reference afterwards. - MediaDataBlock PeekBlock(int trackIndex); - // Consumes the first unconsumed data block on the specified track. It's illegal to call this method if there are no - // unconsumed MediaDataBlock objects left on the specified track. - void ConsumeBlock(int trackIndex); - // Consume all blocks with .StartTime <= startTime from the specified track. - void ConsumeBlocksUntil(int trackIndex, ulong startTime); - } - - public class MuxStateWriter { - private Stream Stream; - public MuxStateWriter(Stream stream) { - this.Stream = stream; - } - public void Close() { - this.Stream.Close(); - } - public void Flush() { - this.Stream.Flush(); - } - // TODO: Use a binary format to save space (maybe 50% of the mux state file size). - public void WriteUlong(char key, ulong num) { - // TODO: Speed this up if necessary. - byte[] outputBytes = Encoding.ASCII.GetBytes(("" + key) + num + '\n'); - this.Stream.Write(outputBytes, 0, outputBytes.Length); - } - public void WriteBytes(char key, byte[] bytes) { - // TODO: Speed this up if necessary. - byte[] outputBytes = Encoding.ASCII.GetBytes(key + ":" + Utils.HexEncodeString(bytes) + '\n'); - this.Stream.Write(outputBytes, 0, outputBytes.Length); - } - public void WriteRaw(byte[] bytes, int start, int end) { - this.Stream.Write(bytes, start, end - start); - } - } - - public class MkvUtils { - private const ulong DATA_SIZE_MAX_VALUE = 72057594037927934uL; - public static byte[] GetDataSizeBytes(ulong value) { - if (value > DATA_SIZE_MAX_VALUE) { - throw new Exception(string.Format("Data size '{0}' is greater than its max value!", value)); - } - byte[] bytes = BitConverter.GetBytes(value); - Array.Reverse(bytes); - int b = 1; - while (value > (1uL << (7 * b)) - 2) { - ++b; - } - byte[] array = new byte[b]; - Buffer.BlockCopy(bytes, 8 - b, array, 0, b); - array[0] += (byte)(1 << (8 - b)); - return array; - } - // Like GetDataSizeBytes, but always returns the longest possible byte array (of size 8). - private static byte[] GetDataSizeEightBytes(ulong value) { - if (value > DATA_SIZE_MAX_VALUE) { - throw new Exception(string.Format("Data size '{0}' is greater than its max value!", value)); - } - byte[] bytes = BitConverter.GetBytes(value); - Array.Reverse(bytes); - bytes[0] = 1; - return bytes; - } - private const ulong VINT_MAX_VALUE = 18446744073709551615uL; - public static byte[] GetVintBytes(ulong value) { - byte[] bytes = BitConverter.GetBytes(value); - Array.Reverse(bytes); - int b = 0; - while (bytes[b] == 0 && b + 1 < bytes.Length) { - ++b; - } - byte[] array = new byte[bytes.Length - b]; - for (int i = 0; i < array.Length; i++) { - array[i] = bytes[b + i]; - } - return array; - } - public static byte[] GetFloatBytes(float value) { - return Utils.InplaceReverseBytes(BitConverter.GetBytes(value)); - } - private static readonly DateTime MinDateTimeValue = DateTime.Parse("2001-01-01").ToUniversalTime(); - public static byte[] GetDateTimeBytes(DateTime dateTime) { - DateTime dateTime2 = dateTime.ToUniversalTime(); - if (dateTime2 < MinDateTimeValue) { - throw new Exception(string.Format("Date '{0}' is lower than its min value!", dateTime.ToShortDateString())); - } - return Utils.InplaceReverseBytes(BitConverter.GetBytes(Convert.ToUInt64( - dateTime2.Subtract(MinDateTimeValue).TotalMilliseconds * 1000000.0))); - } - // Get EBML element bytes. - public static byte[] GetEEBytes(ID id, byte[] contents) { - return Utils.CombineBytes(GetDataSizeBytes((ulong)id), - GetDataSizeBytes((ulong)contents.Length), - contents); - } - private static byte[] GetEbmlHeaderBytes() { - List list = new List(); - list.Add(GetEEBytes(ID.EBMLVersion, GetVintBytes(1uL))); - list.Add(GetEEBytes(ID.EBMLReadVersion, GetVintBytes(1uL))); - list.Add(GetEEBytes(ID.EBMLMaxIDLength, GetVintBytes(4uL))); - list.Add(GetEEBytes(ID.EBMLMaxSizeLength, GetVintBytes(8uL))); - list.Add(GetEEBytes(ID.DocType, Encoding.ASCII.GetBytes("matroska"))); - list.Add(GetEEBytes(ID.DocTypeVersion, GetVintBytes(1uL))); - list.Add(GetEEBytes(ID.DocTypeReadVersion, GetVintBytes(1uL))); - return GetEEBytes(ID.EBML, Utils.CombineByteArrays(list)); - } - public static string GetStringForCodecID(CodecID codecID) { - switch (codecID) { - case CodecID.V_AVC: { return "V_MPEG4/ISO/AVC"; } - case CodecID.V_MS: { return "V_MS/VFW/FOURCC"; } - case CodecID.A_AAC: { return "A_AAC"; } - case CodecID.A_MS: { return "A_MS/ACM"; } - default: { throw new Exception(string.Format("CodecID '{0}' is invalid!", codecID)); } - } - } - public static byte[] GetVideoInfoBytes(ulong pixelWidth, ulong pixelHeight, ulong displayWidth, ulong displayHeight) { - if (pixelWidth == 0uL) { - throw new Exception("PixelWidth must be greater than 0!"); - } - if (pixelHeight == 0uL) { - throw new Exception("PixelHeight must be greater than 0!"); - } - if (displayWidth == 0uL) { - throw new Exception("DisplayWidth must be greater than 0!"); - } - if (displayHeight == 0uL) { - throw new Exception("DisplayHeight must be greater than 0!"); - } - List list = new List(); - list.Add(GetEEBytes(ID.FlagInterlaced, GetVIntForFlag(false))); - list.Add(GetEEBytes(ID.PixelWidth, GetVintBytes(pixelWidth))); - list.Add(GetEEBytes(ID.PixelHeight, GetVintBytes(pixelHeight))); - if (displayWidth != pixelWidth) { - list.Add(GetEEBytes(ID.DisplayWidth, GetVintBytes(displayWidth))); - } - if (displayHeight != pixelHeight) { - list.Add(GetEEBytes(ID.DisplayHeight, GetVintBytes(displayHeight))); - } - return GetEEBytes(ID.Video, Utils.CombineByteArrays(list)); - } - public static byte[] GetAudioInfoBytes(float samplingFrequency, ulong channels, ulong bitDepth) { - if (samplingFrequency <= 0f) { - throw new Exception("SamplingFrequency must be greater than 0!"); - } - if (channels == 0uL) { - throw new Exception("Channels cannot be 0!"); - } - List list = new List(); - list.Add(GetEEBytes(ID.SamplingFrequency, GetFloatBytes(samplingFrequency))); - list.Add(GetEEBytes(ID.Channels, GetVintBytes(channels))); - if (bitDepth != 0uL) { - list.Add(GetEEBytes(ID.BitDepth, GetVintBytes(bitDepth))); - } - return GetEEBytes(ID.Audio, Utils.CombineByteArrays(list)); - } - private static readonly byte[] VINT_FALSE = new byte[] { 0 }; - private static readonly byte[] VINT_TRUE = new byte[] { 1 }; - public static byte[] GetVIntForFlag(bool flag) { - return flag ? VINT_TRUE : VINT_FALSE; - } - private static byte[] GetDurationBytes(ulong duration, ulong timeScale) { - float floatDuration = Convert.ToSingle(duration * 1000.0 / timeScale); - if (floatDuration <= 0f) floatDuration = 0.125f; // .mkv requires a positive duration. - byte[] bytes = BitConverter.GetBytes(floatDuration); // 4 bytes. - Array.Reverse(bytes); - return bytes; - } - - private static byte[] GetSegmentInfoBytes(ulong duration, ulong timeScale, bool isDeterministic) { - AssemblyName name = Assembly.GetEntryAssembly().GetName(); - string muxingApp = name.Name + " v" + name.Version; - string writingApp = muxingApp; - byte[] segmentUid; - if (isDeterministic) { - // 16 bytes; seemingly random, but deterministic. - segmentUid = new byte[] {110, 104, 17, 204, 142, 130, 251, 240, 218, 112, 216, 160, 143, 114, 2, 237}; - } else { - segmentUid = new byte[16]; - new Random().NextBytes(segmentUid); - } - List list = new List(); - // ID.Duration must be the first in the list so FindDurationOffset can find it. - list.Add(GetEEBytes(ID.Duration, GetDurationBytes(duration, timeScale))); - if (!string.IsNullOrEmpty(muxingApp)) { - list.Add(GetEEBytes(ID.MuxingApp, Encoding.ASCII.GetBytes(muxingApp))); - } - if (!string.IsNullOrEmpty(writingApp)) { - list.Add(GetEEBytes(ID.WritingApp, Encoding.ASCII.GetBytes(writingApp))); - } - list.Add(GetEEBytes(ID.SegmentUID, segmentUid)); - // The deterministic date was a few minutes before Tue Apr 17 21:14:22 CEST 2012. - byte[] dateBytes = isDeterministic ? new byte[] {4, 242, 35, 97, 249, 143, 0, 192} - : GetDateTimeBytes(DateTime.UtcNow); - list.Add(GetEEBytes(ID.DateUTC, dateBytes)); - list.Add(GetEEBytes(ID.TimecodeScale, GetVintBytes(timeScale / 10uL))); - return GetEEBytes(ID.Info, Utils.CombineByteArrays(list)); - } - - private static byte[] GetTrackEntriesBytes(IList trackEntries) { - byte[][] byteArrays = new byte[trackEntries.Count][]; - for (int i = 0; i < trackEntries.Count; i++) { - byteArrays[i] = trackEntries[i].GetBytes(); - } - return GetEEBytes(ID.Tracks, Utils.CombineByteArrays(byteArrays)); - } - - // This is a pure data struct. Please don't add functionality. - private struct SeekBlock { - public ID ID; - public ulong Offset; - public SeekBlock(ID id, ulong offset) { - this.ID = id; - this.Offset = offset; - } - } - - private static byte[] GetVoidBytes(ulong length) { - if (length < 9uL) { - // >=9 == 1 byte for ID.Void, 8 bytes for the fixed length and >=0 bytes for the data. - throw new Exception("Void must be greater than or equal to 9 bytes."); - } - length -= 9; - return Utils.CombineBytes(GetDataSizeBytes((ulong)ID.Void), - GetDataSizeEightBytes(length), - new byte[length]); - } - - private static byte[] GetSeekBytes(IList seekBlocks, int desiredSize) { - int seekBlockCount = seekBlocks.Count; - byte[][] byteArrays = new byte[4 * seekBlockCount + 3][]; - byteArrays[0] = GetDataSizeBytes((ulong)ID.SeekHead); - for (int i = 0, j = 2; i < seekBlockCount; ++i, j += 4) { - byteArrays[j] = GetDataSizeBytes((ulong)ID.Seek); - byteArrays[j + 2] = GetEEBytes(ID.SeekID, GetDataSizeBytes((ulong)seekBlocks[i].ID)); - byteArrays[j + 3] = GetEEBytes(ID.SeekPosition, GetVintBytes(seekBlocks[i].Offset)); - byteArrays[j + 1] = GetDataSizeBytes((ulong)(byteArrays[j + 2].Length + byteArrays[j + 3].Length)); - } - int dataSize = 0; - int voidIndex = byteArrays.Length - 1; - for (int i = 2; i < voidIndex; ++i) { - dataSize += byteArrays[i].Length; - } - byteArrays[1] = GetDataSizeBytes((ulong)dataSize); - byteArrays[voidIndex] = new byte[] {}; - if (desiredSize >= 0) { - dataSize += byteArrays[0].Length + byteArrays[1].Length; - if (desiredSize != dataSize) { - if (desiredSize <= dataSize + 9) { - throw new Exception("dataSize too small, got " + dataSize + ", expected <=" + (desiredSize - 9)); - } - byteArrays[voidIndex] = GetVoidBytes((ulong)(desiredSize - dataSize)); - } - } - return Utils.CombineByteArrays(byteArrays); - } - - private const int DESIRED_SEEK_SIZE = 90; - - private static byte[] GetSegmentBytes(ulong duration, ulong mediaEndOffsetMS, - ulong seekHeadOffsetMS, ulong cuesOffsetMS, - ulong timeScale, IList trackEntries, bool isDeterministic) { - byte[][] byteArrays = new byte[5][]; - byteArrays[0] = GetDataSizeBytes((ulong)ID.Segment); // 4 bytes. - // Segment data size. - byteArrays[1] = GetDataSizeEightBytes(mediaEndOffsetMS); // 1 byte header (== 1) + 7 bytes of size. - // byteArrays[2][0] is at segmentOffset. - // byteArrays[2] will be an ID.SeekHead + ID.Void at a total size of DESIRED_SEEK_SIZE. - byteArrays[3] = GetSegmentInfoBytes(duration, timeScale, isDeterministic); - byteArrays[4] = GetTrackEntriesBytes(trackEntries); - - IList seekBlocks = new List(); - seekBlocks.Add(new SeekBlock(ID.Info, DESIRED_SEEK_SIZE)); - seekBlocks.Add(new SeekBlock(ID.Tracks, (ulong)(DESIRED_SEEK_SIZE + byteArrays[3].Length))); - if (seekHeadOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.SeekHead, seekHeadOffsetMS)); - if (cuesOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.Cues, cuesOffsetMS)); - byteArrays[2] = GetSeekBytes(seekBlocks, DESIRED_SEEK_SIZE); - - return Utils.CombineByteArrays(byteArrays); - // * The first 4 bytes of the return value are from GetDataSizeBytes((ulong)ID.Segment). - // * The next 1 byte of the return value is 1, the prefix of the 7-byte data size in datasize.GetUInt64(). - // * The next 7 bytes of the return value the total size of `list', but that doesn't matter, because it would be - // overwritten just after WriteMkv has written all media data and cues to the file (so the total file size is known). - // return GetEEBytes(ID.Segment, GetEBMLBytes(list), true); - } - - // Can't be larger, because the datasize class cannot serialize much larger values than that. - private const ulong INITIAL_MEDIA_END_OFFSET_MS = ulong.MaxValue >> 9; - private const ulong INITIAL_SEEK_HEAD_OFFSET_MS = 0; - private const ulong INITIAL_CUES_OFFSET_MS = 0; - private const ulong KEEP_ORIGINAL_DURATION = ulong.MaxValue - 1; - - // Returns the first offset not updated. - // timeScale is ignored if duration == KEEP_ORIGINAL_DURATION. - private static int UpdatePrefix(byte[] prefix, int prefixSize, - ulong segmentOffset, ulong mediaEndOffsetMS, ulong seekHeadOffsetMS, ulong cuesOffsetMS, - ulong duration, ulong timeScale) { - Buffer.BlockCopy(Utils.InplaceReverseBytes(BitConverter.GetBytes(mediaEndOffsetMS)), 1, - prefix, (int)segmentOffset - 7, 7); - int durationOffset; - int afterInfoOffset; - FindDurationAndAfterInfoOffset(prefix, (int)segmentOffset, prefixSize, out durationOffset, out afterInfoOffset); - if (duration != KEEP_ORIGINAL_DURATION) { - Buffer.BlockCopy(GetDurationBytes(duration, timeScale), 0, prefix, durationOffset, 4); - } - IList seekBlocks = new List(); - seekBlocks.Add(new SeekBlock(ID.Info, DESIRED_SEEK_SIZE)); - seekBlocks.Add(new SeekBlock(ID.Tracks, (ulong)afterInfoOffset - segmentOffset)); - if (seekHeadOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.SeekHead, seekHeadOffsetMS)); - if (cuesOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.Cues, cuesOffsetMS)); - byte[] seekBytes = GetSeekBytes(seekBlocks, DESIRED_SEEK_SIZE); - Buffer.BlockCopy(seekBytes, 0, prefix, (int)segmentOffset, seekBytes.Length); - return durationOffset + 4; - } - - private static int GetEbmlElementDataSize(byte[] bytes, ref int i) { - // Width Size Representation - // 1 2^7 1xxx xxxx - // 2 2^14 01xx xxxx xxxx xxxx - // 3 2^21 001x xxxx xxxx xxxx xxxx xxxx - // 4 2^28 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx - // ..7. - if (bytes.Length <= i) { - throw new Exception("EOF in EBML length."); - } - if ((bytes[i] & 0x80) != 0) { - return bytes[i++] & 0x7f; - } else if ((bytes[i] & 0x40) != 0) { - i += 2; - if (bytes.Length < i) { - throw new Exception("EOF in EBML length 2."); - } - return (bytes[i - 2] & 0x3f) << 8 | bytes[i - 1]; - } else if (bytes[i] == 1) { - i += 8; - if (bytes.Length < i) { - throw new Exception("EOF in EBML length 8."); - } - if (bytes[i - 5] != 0 || bytes[i - 6] != 0 || bytes[i - 7] != 0 || (bytes[i - 4] & 0x80) != 0) { - throw new Exception("EBML length 8 too large for an int."); - } - return bytes[i - 1] | bytes[i - 2] << 8 | bytes[i - 3] << 16 | bytes[i - 4] << 24; - } else { - throw new Exception("Long EBML elements not implemented."); - } - } - - // Sets durationOffset to the 4 bytes in `bytes' containing the floatDuration field. - // Sets afterInfoOffset to the offset right after the ID.Info element. - // `bytes' is the prefix on an .mkv file written by us, with ID.Segment starting at segmentOffset or 0. - // `j' is the end offset in bytes. - private static void FindDurationAndAfterInfoOffset(byte[] bytes, int segmentOffset, int j, - out int durationOffset, out int afterInfoOffset) { - int i = segmentOffset; - // Skip ID.EBML if present. - // if (i + 4 <= j && bytes[i] == 26 && bytes[i + 1] == 69 && bytes[i + 2] == 223 && bytes[i + 3] == 163) { - // i += 4; i += ... GetEbmlElementDataSize(bytes, ref i); - // } - // Skip ID.SeekHead if present. - if (i + 4 <= j && bytes[i] == 17 && bytes[i + 1] == 77 && bytes[i + 2] == 155 && bytes[i + 3] == 116) { - i += 4; - int n = GetEbmlElementDataSize(bytes, ref i); // Doesn't work (i becomes 68 instead of 76) without a helper. - i += n; - } - // Skip ID.Void if present. - if (i < j && bytes[i] == 236) { - ++i; - int n = GetEbmlElementDataSize(bytes, ref i); // Doesn't work (i becomes 68 instead of 76) without a helper. - i += n; - } - // Detect ID.Info. - if (!(i + 4 <= j && bytes[i] == 21 && bytes[i + 1] == 73 && bytes[i + 2] == 169 && bytes[i + 3] == 102)) { - throw new Exception("Expected ID.Info."); - } - i += 4; - int infoSize = GetEbmlElementDataSize(bytes, ref i); - afterInfoOffset = i + infoSize; - if (j > i + infoSize) j = i + infoSize; - // Detect ID.Duration. - if (!(i + 2 <= j && bytes[i] == 68 && bytes[i + 1] == 137)) { - throw new Exception("Expected ID.Duration."); - } - i += 2; - int durationSize = GetEbmlElementDataSize(bytes, ref i); - if (durationSize != 4) { - throw new Exception("Bad durationSize."); - } - durationOffset = i; - } - - private static int GetVideoTrackIndex(IList trackEntries, int defaultIndex) { - int videoTrackIndex = 0; - while (videoTrackIndex < trackEntries.Count && trackEntries[videoTrackIndex].TrackType != TrackType.Video) { - ++videoTrackIndex; - } - return (videoTrackIndex == trackEntries.Count) ? defaultIndex : videoTrackIndex; - } - - private static IList GetIsAmsCodecs(IList trackEntries) { - IList isAmsCodecs = new List(); - for (int i = 0; i < trackEntries.Count; ++i) { - isAmsCodecs.Add(trackEntries[i].CodecID == CodecID.A_MS); - } - return isAmsCodecs; - } - - private static byte[] GetSimpleBlockBytes(ulong trackNumber, short timeCode, bool IsKeyFrame, bool isAmsCodec, - int mediaDataBlockTotalSize) { - // Was: LacingID lacingId = isAmsCodec ? LacingID.FixedSize : LacingID.No; - byte b = isAmsCodec ? (byte)4 : (byte)0; - if (IsKeyFrame) { - b += 128; - } - // Originally b was always initialized to 0, and then incremented like this: - // switch (lacingId) { - // case LacingID.No: { break; } - // case LacingID.Xiph: { b += 2; break; } - // case LacingID.EBML: { b += 6; break; } - // case LacingID.FixedSize: { b += 4; break; } - // } - List output = new List(); - output.Add(GetDataSizeBytes((ulong)ID.SimpleBlock)); - output.Add(null); // Reserved for the return value of GetDataSizeBytes. - output.Add(GetDataSizeBytes(trackNumber)); - output.Add(Utils.InplaceReverseBytes(BitConverter.GetBytes(timeCode))); - output.Add(new byte[] { b }); - // Was: if (lacingId != LacingID.No) output.Add(new byte[] { (byte)(sampleData.Count - 1) }); - if (isAmsCodec) output.Add(new byte[] { (byte)1 }); - int totalSize = 0; - for (int i = 2; i < output.Count; ++i) { - totalSize += output[i].Length; - } - output[1] = GetDataSizeBytes((ulong)(totalSize + mediaDataBlockTotalSize)); - // Usually output[0].Length == 3, and the length of the rest of output (without sampleData) is 4. - return Utils.CombineByteArrays(output); - } - - public static byte[] GetCueBytes(IList cuePoints) { - byte[][] output = new byte[cuePoints.Count][]; - for (int i = 0; i < cuePoints.Count; i++) { - output[i] = cuePoints[i].GetBytes(); - } - // TODO: Avoid unnecessary copies, also in GetEEBytes. - return GetEEBytes(ID.Cues, Utils.CombineByteArrays(output)); - } - - private class StateChunkStartTimeReceiver : IChunkStartTimeReceiver { - private MuxStateWriter MuxStateWriter; - private IList[] TrackChunkStartTimes; - private int[] TrackChunkWrittenCounts; - // Takes ownership of trackChunkStartTimes (and will append to its items). - public StateChunkStartTimeReceiver(MuxStateWriter muxStateWriter, IList[] trackChunkStartTimes) { - this.MuxStateWriter = muxStateWriter; - this.TrackChunkStartTimes = trackChunkStartTimes; - this.TrackChunkWrittenCounts = new int[trackChunkStartTimes.Length]; // Initializes items to 0. - for (int trackIndex = 0; trackIndex < trackChunkStartTimes.Length; ++trackIndex) { - IList chunkStartTimes = trackChunkStartTimes[trackIndex]; - if (chunkStartTimes == null) { - trackChunkStartTimes[trackIndex] = chunkStartTimes = new List(); - } else { - int chunkCount = chunkStartTimes.Count; - for (int chunkIndex = 1; chunkIndex < chunkCount; ++chunkIndex) { - if (chunkStartTimes[chunkIndex - 1] >= chunkStartTimes[chunkIndex]) { - throw new Exception(string.Concat(new object[] { - "Chunk StartTimes not increasing: track=", trackIndex, - " chuunk=", chunkIndex })); - } - } - } - this.TrackChunkWrittenCounts[trackIndex] = trackChunkStartTimes[trackIndex].Count; - } - } - /*implements*/ public ulong GetChunkStartTime(int trackIndex, int chunkIndex) { - IList chunkStartTimes = this.TrackChunkStartTimes[trackIndex]; - return chunkIndex >= chunkStartTimes.Count ? ulong.MaxValue : chunkStartTimes[chunkIndex]; - } - /*implements*/ public void SetChunkStartTime(int trackIndex, int chunkIndex, ulong chunkStartTime) { - int chunkCount = this.TrackChunkStartTimes[trackIndex].Count; - if (chunkIndex == chunkCount) { // A simple append. - IList chunkStartTimes = this.TrackChunkStartTimes[trackIndex]; - if (chunkCount > 0) { - ulong lastChunkStartTime = chunkStartTimes[chunkCount - 1]; - if (lastChunkStartTime >= chunkStartTime) { - throw new Exception(string.Concat(new object[] { - "New chunk StartTime not larger: track=", trackIndex, " chunk=", chunkIndex, - " last=", lastChunkStartTime, " new=", chunkStartTime })); - } - } - chunkStartTimes.Add(chunkStartTime); - ++chunkCount; - // Flush all chunk StartTimes not written to the .muxstate yet. Usually we write only one item - // (chunkStartTime) here. - int i = this.TrackChunkWrittenCounts[trackIndex]; - char key = (char)('n' + trackIndex); - if (i == 0) this.MuxStateWriter.WriteUlong(key, chunkStartTimes[i++]); - for (; i < chunkCount; ++i) { - this.MuxStateWriter.WriteUlong(key, chunkStartTimes[i] - chunkStartTimes[i - 1]); - } - // There is no need to call this.MuxStateWriter.Flush(); here. it's OK to flush that later. - this.TrackChunkWrittenCounts[trackIndex] = i; - } else if (chunkIndex < chunkCount) { - ulong oldChunkStartTime = this.TrackChunkStartTimes[trackIndex][chunkIndex]; - if (chunkStartTime != oldChunkStartTime) { - throw new Exception(string.Concat(new object[] { - "Chunk StartTime mismatch: track=", trackIndex, " chunk=", chunkIndex, - " old=", oldChunkStartTime, " new=", chunkStartTime })); - } - } else { - throw new Exception(string.Concat(new object[] { - "Chunk StartTime set too far: track=", trackIndex, " chunk=", chunkIndex, - " chunkCount=" + chunkCount })); - } - } - } - - // Calls fileStream.Position and fileStream.Write only. - // - // Usually trackSamples has 2 elements: a video track and an audio track. - // - // Uses trackEntries and trackSamples only as a read-only argument, doesn't modify their contents. - // - // Starts with the initial cue points specified in cuePoints, and appends subsequent cue points in place. - private static void WriteClustersAndCues(FileStream fileStream, - ulong segmentOffset, - int videoTrackIndex, - IList isAmsCodecs, - IMediaDataSource mediaDataSource, - MuxStateWriter muxStateWriter, - IList cuePoints, - ref ulong minStartTime, - ulong timePosition, - out ulong seekHeadOffsetMS, - out ulong cuesOffsetMS) { - int trackCount = mediaDataSource.GetTrackCount(); - if (isAmsCodecs.Count != trackCount) { - throw new Exception("ASSERT: isAmsCodecs vs mediaDataSource length mismatch."); - } - if (trackCount > 13) { // 13 is because a..m and n..z in MuxStateWriter checkpointing. - throw new Exception("Too many tracks to mux."); - } - // For each track, contains the data bytes of a media sample ungot (i.e. pushed back) after reading. - // Initializes items to null (good). - MediaDataBlock[] ungetBlocks = new MediaDataBlock[trackCount]; - ulong minStartTime0 = minStartTime; - if (timePosition == ulong.MaxValue) { - timePosition = 0; - ulong maxStartTime = ulong.MaxValue; - for (int i = 0; i < trackCount; ++i) { - if ((ungetBlocks[i] = mediaDataSource.PeekBlock(i)) != null) { - if (maxStartTime == ulong.MaxValue || maxStartTime < ungetBlocks[i].StartTime) { - maxStartTime = ungetBlocks[i].StartTime; - } - mediaDataSource.ConsumeBlock(i); // Since it was moved to ungetBlocks[i]. - } - } - for (int i = 0; i < trackCount; ++i) { - MediaDataBlock block = mediaDataSource.PeekBlock(i); - while (block != null && block.StartTime <= maxStartTime) { - ungetBlocks[i] = block; // Takes ownership. - mediaDataSource.ConsumeBlock(i); - } - // We'll start each track (in ungetMediaSample[i]) from the furthest sample within maxStartTime. - } - int trackIndex2; - if ((trackIndex2 = GetNextTrackIndex(mediaDataSource, ungetBlocks)) < 0) { - throw new Exception("ASSERT: Empty media file, no samples."); - } - minStartTime = minStartTime0 = ungetBlocks[trackIndex2] != null ? ungetBlocks[trackIndex2].StartTime : - mediaDataSource.PeekBlock(trackIndex2).StartTime; - muxStateWriter.WriteUlong('A', minStartTime0); - } - List> output = new List>(); - ulong[] lastOutputStartTimes = new ulong[trackCount]; // Items initialized to zero. - int trackIndex; - // timePosition is the beginning StartTime of the last output block written by fileStream.Write. - while ((trackIndex = GetNextTrackIndex(mediaDataSource, ungetBlocks)) >= 0) { - ulong timeCode; // Will be set below. - bool isKeyFrame; // Will be set below. - MediaDataBlock block0; // Will be set below. - MediaDataBlock block1 = null; // May be set below. - int mediaDataBlockTotalSize; // Will be set below. - { - if ((block0 = ungetBlocks[trackIndex]) == null && - (block0 = mediaDataSource.PeekBlock(trackIndex)) == null) { - throw new Exception("ASSERT: Reading from a track already at EOF."); - } - // Some kind of time delta for this sample. - timeCode = block0.StartTime - timePosition - minStartTime0; - if (block0.StartTime < timePosition + minStartTime0) { - throw new Exception("Bad start times: block0.StartTime=" + block0.StartTime + - " timePosition=" + timePosition + " minStartTime=" + minStartTime0); - } - isKeyFrame = block0.IsKeyFrame; - mediaDataBlockTotalSize = block0.Bytes.Count; - if (ungetBlocks[trackIndex] != null) { - ungetBlocks[trackIndex] = null; - } else { - mediaDataSource.ConsumeBlock(trackIndex); - } - } - if (timeCode > 327670000uL) { - throw new Exception("timeCode too large: " + timeCode); // Maybe that's not fatal? - } - if (isAmsCodecs[trackIndex]) { // Copy one more MediaSample if available. - // TODO: Test this. - block1 = ungetBlocks[trackIndex]; - if (block1 != null) { - mediaDataBlockTotalSize += block1.Bytes.Count; - ungetBlocks[trackIndex] = null; - } else if ((block1 = mediaDataSource.PeekBlock(trackIndex)) != null) { - mediaDataBlockTotalSize += block1.Bytes.Count; - mediaDataSource.ConsumeBlock(trackIndex); - } - } - // TODO: How can be timeCode so large at this point? - if ((output.Count != 0 && trackIndex == videoTrackIndex && isKeyFrame) || timeCode > 327670000uL) { - ulong outputOffset = (ulong)fileStream.Position - segmentOffset; - cuePoints.Add(new CuePoint(timePosition / 10000uL, (ulong)(videoTrackIndex + 1), outputOffset)); - muxStateWriter.WriteUlong('C', timePosition); - muxStateWriter.WriteUlong('D', outputOffset); - int totalSize = 0; - for (int i = 0; i < output.Count; ++i) { - totalSize += output[i].Count; - } - // We do a single copy of the media stream data bytes here. That copy is inevitable, because it's - // faster to save to file that way. - byte[] bytes = Utils.CombineByteArraysAndArraySegments( - new byte[][]{GetDataSizeBytes((ulong)ID.Cluster), GetDataSizeBytes((ulong)totalSize)}, output); - output.Clear(); - // The average bytes.Length is 286834 bytes here, that's large enough (>8 kB), and it doesn't warrant a - // a buffered output stream for speedup. - fileStream.Write(bytes, 0, bytes.Length); - fileStream.Flush(); - for (int i = 0; i < trackCount; ++i) { - muxStateWriter.WriteUlong((char)('a' + i), lastOutputStartTimes[i]); - } - muxStateWriter.WriteUlong('P', (ulong)bytes.Length); - muxStateWriter.Flush(); - } - if (output.Count == 0) { - timePosition += timeCode; - timeCode = 0uL; - output.Add(new ArraySegment( - GetEEBytes(ID.Timecode, GetVintBytes(timePosition / 10000uL)))); - } - output.Add(new ArraySegment(GetSimpleBlockBytes( - (ulong)(trackIndex + 1), (short)(timeCode / 10000uL), isKeyFrame, isAmsCodecs[trackIndex], - mediaDataBlockTotalSize))); - output.Add(block0.Bytes); - if (block1 != null) output.Add(block1.Bytes); - lastOutputStartTimes[trackIndex] = block1 != null ? block1.StartTime : block0.StartTime; - } - - // Write remaining samples (from output to fileStream), and write cuePoints. - { - ulong outputOffset = (ulong)fileStream.Position - segmentOffset; - cuePoints.Add(new CuePoint(timePosition / 10000uL, (ulong)(videoTrackIndex + 1), outputOffset)); - muxStateWriter.WriteUlong('C', timePosition); - muxStateWriter.WriteUlong('D', outputOffset); - if (output.Count == 0) { - throw new Exception("ASSERT: Expecting non-empty output at end of mixing."); - } - int totalSize = 0; - for (int i = 0; i < output.Count; ++i) { - totalSize += output[i].Count; - } - byte[] bytes = Utils.CombineByteArraysAndArraySegments( - new byte[][]{GetDataSizeBytes((ulong)ID.Cluster), GetDataSizeBytes((ulong)totalSize)}, output); - output.Clear(); // Save memory. - cuesOffsetMS = outputOffset + (ulong)bytes.Length; - byte[] bytes2 = GetCueBytes(cuePoints); // cues are about 1024 bytes per 2 minutes. - seekHeadOffsetMS = cuesOffsetMS + (ulong)bytes2.Length; - SeekBlock[] seekBlocks = new SeekBlock[cuePoints.Count]; - for (int i = 0; i < cuePoints.Count; ++i) { - seekBlocks[i] = new SeekBlock(ID.Cluster, cuePoints[i].CueClusterPosition); - } - byte[] bytes3 = GetSeekBytes(seekBlocks, -1); - bytes = Utils.CombineBytes(bytes, bytes2, bytes3); - fileStream.Write(bytes, 0, bytes.Length); - } - } - - // Returns trackIndex with the smallest StartTime, or -1. - private static int GetNextTrackIndex(IMediaDataSource mediaDataSource, MediaDataBlock[] ungetBlocks) { - int trackCount = ungetBlocks.Length; // == mediaDataSource.GetTrackCount(). - ulong minUnconsumedStartTime = 0; // No real need to initialize it here. - int trackIndex = -1; - for (int i = 0; i < trackCount; ++i) { - MediaDataBlock block = ungetBlocks[i]; - if (block == null) block = mediaDataSource.PeekBlock(i); - if (block != null && (trackIndex == -1 || minUnconsumedStartTime > block.StartTime)) { - trackIndex = i; - minUnconsumedStartTime = block.StartTime; - } - } - return trackIndex; - } - - private const ulong MUX_STATE_VERSION = 923840374526694867; - - // This is a pure data class, please don't add logic. - private class ParsedMuxState { - public string status; - public bool hasZ; - public ulong vZ; - public bool hasM; - public ulong vM; - public bool hasS; - public ulong vS; - public bool hasA; - public ulong vA; - public bool isXGood; - public bool hasX; - public ulong vX; - public bool hasV; - public ulong vV; - public bool hasH; - public byte[] vH; - public IList cuePoints; - public bool isComplete; - public bool isContinuable; - public ulong lastOutOfs; - public bool hasC; - public ulong lastC; - // this.trackLastStartTimes[trackIndex] is a StartTime lower limit. When muxing is continued, MediaDataBlock()s with - // .StartTime <= the limit must be ignored (consumed). - public ulong[] trackLastStartTimes; - public IList[] trackChunkStartTimes; - public int endOffset; - public ParsedMuxState() { - this.isXGood = false; - this.hasZ = this.hasM = this.hasS = this.hasX = this.hasV = this.hasH = this.hasC = this.hasA = false; - this.isComplete = this.isContinuable = false; - this.vZ = this.vM = this.vS = this.vX = this.vV = this.vA = 0; - this.vH = null; - this.status = "unparsed"; - this.cuePoints = null; - this.lastOutOfs = 0; - this.trackLastStartTimes = null; - this.trackChunkStartTimes = null; - this.endOffset = 0; - this.lastC = 0; - } - public override String ToString() { - StringBuilder buf = new StringBuilder(); - buf.Append("ParsedMuxState(status=" + Utils.EscapeString(this.status)); - if (this.isComplete) { - buf.Append(", Complete"); - } else if (this.isContinuable) { - buf.Append(", Continuable"); - } else { - buf.Append(", Unusable"); - } - if (this.isXGood) { - buf.Append(", XGood"); - } else if (this.hasX) { - buf.Append(", X=" + this.vX); - } - if (this.hasS) { - buf.Append(", S=" + this.vS); - } - if (this.hasH) { - buf.Append(", H.size=" + this.vH.Length); - } - if (this.hasA) { - buf.Append(", A=" + this.vA); - } - if (this.hasV) { - buf.Append(", V=" + this.vV); - } - if (this.hasC) { - buf.Append(", lastC=" + this.lastC); - } - if (this.hasM) { - buf.Append(", M=" + this.vM); - } - if (this.hasZ) { - buf.Append(", Z=" + this.vZ); - } - if (this.cuePoints != null) { - buf.Append(", cuePoints.size=" + this.cuePoints.Count); - } - if (this.lastOutOfs > 0) { - buf.Append(", lastOutOfs=" + this.lastOutOfs); - } - if (this.trackLastStartTimes != null) { - for (int i = 0; i < this.trackLastStartTimes.Length; ++i) { - buf.Append(", lastStartTime[" + i + "]=" + this.trackLastStartTimes[i]); - } - } - if (this.trackChunkStartTimes != null) { - for (int i = 0; i < this.trackChunkStartTimes.Length; ++i) { - buf.Append(", chunkStartTime[" + i + "].Size=" + this.trackChunkStartTimes[i].Count); - } - } - buf.Append(")"); - return buf.ToString(); - } - } - - private static ParsedMuxState ParseMuxState(byte[] muxState, ulong oldSize, byte[] prefix, int prefixSize, - int videoTrackIndex, int trackCount) { - ParsedMuxState parsedMuxState = new ParsedMuxState(); - if (muxState == null) { - parsedMuxState.status = "no mux state"; - return parsedMuxState; - } - if (muxState.Length == 0) { - parsedMuxState.status = "empty mux state"; - return parsedMuxState; - } - if (oldSize == 0) { - parsedMuxState.status = "empty old file"; - return parsedMuxState; - } - if (prefixSize == 0) { - parsedMuxState.status = "empty old prefix"; - return parsedMuxState; - } - - // muxState might be truncated, so we find a sensible end offset to parse until. - int end = 0; - byte b; - int i = muxState.Length; - int j; - if (i > 0 && (b = muxState[i - 1]) != '\n' && b != '\r') { // Ignore the last, incomplete line. - while (i > 0 && (b = muxState[i - 1]) != '\n' && b != '\r') { - --i; - } - if (i > 0) --i; - } - for (;;) { // Traverse the lines backwards. - while (i > 0 && ((b = muxState[i - 1]) == '\n' || b == '\r')) { - --i; - } - if (i == 0) break; - j = i; - while (i > 0 && (b = muxState[i - 1]) != '\n' && b != '\r') { - --i; - } - // Found non-empty line muxState[i : j] (without trailing newlines). - // Console.WriteLine("(" + Encoding.ASCII.GetString(Utils.GetSubBytes(muxState, i, j)) + ")"); - if (muxState[i] == 'Z' || muxState[i] == 'P') { // Stop just after the last line starting with Z or P. - end = j + 1; // +1 for the trailing newline. - break; - } - } - if (end == 0) { - parsedMuxState.status = "truncated to useless"; - return parsedMuxState; - } - parsedMuxState.endOffset = end; - - // Parse muxState[:end]. - // Output block state. Values: - // * -5: expecting Z or after Z - // * -4: expecting X, S, H or V - // * -3: expecting A - // * -2: expecting C - // * -1: expecting D - // * 0: expecting a (trackIndex == 0) or M - // * 1: expecting b (trackIndex == 1) - // * ... - // * trackCount: expecting P - int outState = -4; // Output block state: -1 before V, - parsedMuxState.lastC = ulong.MaxValue; - ulong lastD = ulong.MaxValue; - i = 0; - if (i >= end || muxState[i] != 'X') { - parsedMuxState.status = "expected key X in the beginning"; - return parsedMuxState; - } - while (i < end) { - byte key = muxState[i++]; - if (key == '\r' || key == '\n') continue; - bool doCheckDup = false; - if (key == 'X' || key == 'S' || key == 'V' || key == 'A' || key == 'M' || key == 'Z' || - key == 'C' || key == 'D' || key == 'P' || (uint)(key - 'a') < 26) { - ulong v = 0; - while (i < end && (b = muxState[i]) != '\n' && b != '\r') { - if (((uint)b - '0') > 9) { - parsedMuxState.status = "expected ulong for key " + (char)key; - return parsedMuxState; - } - if (v > (ulong.MaxValue - (ulong)(b - '0')) / 10) { - parsedMuxState.status = "ulong overflow for key " + (char)key; - return parsedMuxState; - } - v = 10 * v + (ulong)(b - '0'); - ++i; - } - if (i == end) { - parsedMuxState.status = "EOF in key " + (char)key; - return parsedMuxState; - } - if (key == 'X' && outState == -4) { - doCheckDup = parsedMuxState.hasX; parsedMuxState.hasX = true; parsedMuxState.vX = v; - parsedMuxState.isXGood = (v == MUX_STATE_VERSION); - if (!parsedMuxState.isXGood) { - parsedMuxState.status = "unsupported format version (X)"; - return parsedMuxState; - } - } else if (key == 'S' && outState == -4) { - doCheckDup = parsedMuxState.hasS; parsedMuxState.hasS = true; parsedMuxState.vS = v; - } else if (key == 'V' && outState == -4) { - doCheckDup = parsedMuxState.hasV; parsedMuxState.hasV = true; parsedMuxState.vV = v; - outState = -3; - } else if (key == 'A' && outState == -3) { - doCheckDup = parsedMuxState.hasA; parsedMuxState.hasA = true; parsedMuxState.vA = v; - outState = -2; - } else if (key == 'M' && outState == 0) { - doCheckDup = parsedMuxState.hasM; parsedMuxState.hasM = true; parsedMuxState.vM = v; - outState = -5; - } else if (key == 'Z' && outState == -5) { - doCheckDup = parsedMuxState.hasZ; parsedMuxState.hasZ = true; parsedMuxState.vZ = v; - } else if (key == 'C' && outState == -2) { - outState = -1; - parsedMuxState.lastC = v; - } else if (key == 'D' && outState == -1) { - outState = 0; - if (parsedMuxState.cuePoints == null) { - parsedMuxState.cuePoints = new List(); - } - parsedMuxState.cuePoints.Add(new CuePoint( - parsedMuxState.lastC / 10000uL, (ulong)(videoTrackIndex + 1), v)); - lastD = v; - if (parsedMuxState.trackLastStartTimes == null) { - parsedMuxState.trackLastStartTimes = new ulong[trackCount]; // Initialized to 0. Good. - } - } else if ((uint)(key - 'a') < 13 && outState < trackCount && outState == key - 'a') { - if (v <= parsedMuxState.trackLastStartTimes[outState]) { - parsedMuxState.status = "trackLastStart time values must increase, got " + v + - ", expected > " + parsedMuxState.trackLastStartTimes[outState]; - return parsedMuxState; - } - parsedMuxState.trackLastStartTimes[outState] = v; - ++outState; - } else if ((uint)(key - 'n') < 13 && outState >= -3) { - if (parsedMuxState.trackChunkStartTimes == null) { - parsedMuxState.trackChunkStartTimes = new IList[trackCount]; - for (int ti = 0; ti < trackCount; ++ti) { - parsedMuxState.trackChunkStartTimes[ti] = new List(); - } - } - int trackIndex = key - 'n'; - int chunkCount = parsedMuxState.trackChunkStartTimes[trackIndex].Count; - if (chunkCount > 0) { - ulong lastChunkStartTime = - parsedMuxState.trackChunkStartTimes[trackIndex][chunkCount - 1]; - v += lastChunkStartTime; - if (lastChunkStartTime >= v) { - parsedMuxState.status = string.Concat(new object[] { - "trackChunkStartTime values must increase, got ", v, ", expected > ", - lastChunkStartTime, " for track ", trackIndex }); - return parsedMuxState; - } - } - parsedMuxState.trackChunkStartTimes[trackIndex].Add(v); - } else if (key == 'P' && outState == trackCount) { - outState = -2; - parsedMuxState.lastOutOfs = v + parsedMuxState.vS + lastD; - lastD = ulong.MaxValue; // A placeholder to expose future bugs. - } else { - parsedMuxState.status = "unexpected key " + (char)key + " in outState " + outState; - return parsedMuxState; - } - } else if (key == 'H') { - if (i == end || muxState[i] != ':') { - parsedMuxState.status = "expected colon after hex key " + (char)key; - return parsedMuxState; - } - j = ++i; - while (i > 0 && (b = muxState[i]) != '\n' && b != '\r') { - ++i; - } - byte[] bytes = Utils.HexDecodeBytes(muxState, j, i); - if (bytes == null) { - parsedMuxState.status = "parse error in hex key " + (char)key; - return parsedMuxState; - } - if (key == 'H' && outState == -4) { - doCheckDup = parsedMuxState.hasH; parsedMuxState.hasH = true; parsedMuxState.vH = bytes; - } else { - parsedMuxState.status = "unexpected key " + (char)key + " in outState " + outState; - return parsedMuxState; - } - } else { - parsedMuxState.status = "unknown key " + (char)key; - return parsedMuxState; - } - if (doCheckDup) { - parsedMuxState.status = "duplicate key " + (char)key; - return parsedMuxState; - } - } - if (outState != -5 && outState != -2) { - parsedMuxState.status = "unexpected final outState " + outState; - return parsedMuxState; - } - if (!parsedMuxState.hasV) { - parsedMuxState.status = "missing video track index (V)"; - return parsedMuxState; - } - if (parsedMuxState.vV != (ulong)videoTrackIndex) { - parsedMuxState.status = "video track index (V) mismatch, expected " + videoTrackIndex; - return parsedMuxState; - } - if (!parsedMuxState.hasH) { - parsedMuxState.status = "missing hex file prefix (H)"; - return parsedMuxState; - } - if (parsedMuxState.vH.Length < 10) { - // This shouldn't happen, because we read 4096 bytes below, and the header is usually just 404 bytes long. - parsedMuxState.status = "hex file prefix (H) too short"; - return parsedMuxState; - } - if (parsedMuxState.vH.Length > prefixSize) { - // This shouldn't happen, because we read 4096 bytes below, and the header is usually just 404 bytes long. - parsedMuxState.status = "hex file prefix (H) too long, maximum prefix size is " + prefixSize; - return parsedMuxState; - } - if (!parsedMuxState.hasS) { - parsedMuxState.status = "missing segmentOffset (S)"; - return parsedMuxState; - } - if (parsedMuxState.vS < 10 || parsedMuxState.vS > oldSize) { - parsedMuxState.status = "bad video track index (V) range"; - return parsedMuxState; - } - if (!parsedMuxState.hasA) { - parsedMuxState.status = "missing minStartTime (A)"; - return parsedMuxState; - } - if (parsedMuxState.hasZ) { - if (parsedMuxState.vZ != 1) { - parsedMuxState.status = "bad end marker (Z) value, expected 1"; - return parsedMuxState; - } - if (!parsedMuxState.hasM) { - parsedMuxState.status = "missing key M"; - return parsedMuxState; - } - if (parsedMuxState.vM != oldSize) { - parsedMuxState.status = "old file size (M) mismatch, expected " + oldSize; - return parsedMuxState; - } - } - // Console.WriteLine("H(" + Utils.HexEncodeString(Utils.GetSubBytes(prefix, 0, parsedMuxState.vH.Length)) + ")"); - if (!Utils.ArePrefixBytesEqual(parsedMuxState.vH, prefix, parsedMuxState.vH.Length)) { - // We repeat the comparison with the mediaEndOffsetMS, seekHeadOffsetMS, cuesOffsetMS and duration fields - // ignored. (So compare e.g. the .mkv format version and the track codec parameters.) - // - // If not complete yet (!parsedMuxState.hasZ), then the duration may be different; the other fields may be - // different as well if WriteMkv has written the output of UpdatePrefix, but not the 'Z' value yet. If complete, - // all the fields may be different (usually the duration is the same, and the other fields are different, - // because they don't contain their INITIAL_* value anymore). - // - // We ignore the duration by copying it from one array to the other (it could work the other way around as well). - // We ignore the other fields by setting them back to their INITIAL_* values before the comparison. - int prefixCompareSize = parsedMuxState.vH.Length; // Shortness is checked above. - byte[] prefix1 = new byte[prefixCompareSize]; - Buffer.BlockCopy(parsedMuxState.vH, 0, prefix1, 0, prefixCompareSize); - byte[] prefix2 = new byte[prefixCompareSize]; - Buffer.BlockCopy(prefix, 0, prefix2, 0, prefixCompareSize); - UpdatePrefix( // TODO: Catch exception if this fails. - prefix2, prefixCompareSize, parsedMuxState.vS, - INITIAL_MEDIA_END_OFFSET_MS, INITIAL_SEEK_HEAD_OFFSET_MS, INITIAL_CUES_OFFSET_MS, - KEEP_ORIGINAL_DURATION, /*timeScale:*/0); - int durationOffset; - int afterInfoOffset; // Ignored, dummy. - // TODO: Catch exception if this fails. - FindDurationAndAfterInfoOffset(prefix1, (int)parsedMuxState.vS, prefixCompareSize, - out durationOffset, out afterInfoOffset); - Buffer.BlockCopy(prefix1, durationOffset, prefix2, durationOffset, 4); - if (!Utils.ArePrefixBytesEqual(prefix1, prefix2, prefixCompareSize)) { - // Console.WriteLine("P(" + Utils.HexEncodeString(Utils.GetSubBytes(prefix, 0, parsedMuxState.vH.Length)) + ")"); - // Console.WriteLine("V(" + Utils.HexEncodeString(Utils.GetSubBytes(parsedMuxState.vH, 0, parsedMuxState.vH.Length)) + ")"); - parsedMuxState.status = "hex file prefix (H) mismatch"; - return parsedMuxState; - } - } - if (parsedMuxState.hasZ) { - parsedMuxState.isComplete = true; - parsedMuxState.status = "complete"; - } else if (parsedMuxState.cuePoints == null || parsedMuxState.cuePoints.Count == 0) { - parsedMuxState.status = "no cue points"; - } else if (parsedMuxState.lastOutOfs <= parsedMuxState.vS) { - parsedMuxState.status = "no downloaded media data"; - } else if (parsedMuxState.lastOutOfs > oldSize) { - parsedMuxState.status = "file shorter than lastOutOfs"; - } else if (parsedMuxState.trackChunkStartTimes == null) { - parsedMuxState.status = "no chunk start times"; - } else { - if (parsedMuxState.trackLastStartTimes == null) { - throw new Exception("ASSERT: expected trackLastStartTimes."); - } - for (i = 0; i < trackCount; ++i) { - if (parsedMuxState.trackLastStartTimes[i] == 0) { - throw new Exception("ASSERT: expected positive trackLastStartTimes value."); - } - } - parsedMuxState.isContinuable = true; - parsedMuxState.status = "continuable"; - } - return parsedMuxState; - } - - // This function may modify trackSamples in a destructive way, to save memory. - public static void WriteMkv(string mkvPath, - IList trackEntries, - IMediaDataSource mediaDataSource, - ulong maxTrackEndTimeHint, - ulong timeScale, - bool isDeterministic, - byte[] oldMuxState, - MuxStateWriter muxStateWriter) { - if (trackEntries.Count != mediaDataSource.GetTrackCount()) { - throw new Exception("ASSERT: trackEntries vs mediaDataSource length mismatch."); - } - bool doParseOldMuxState = oldMuxState != null && oldMuxState.Length > 0; - FileMode fileMode = doParseOldMuxState ? FileMode.OpenOrCreate : FileMode.Create; - using (FileStream fileStream = new FileStream(mkvPath, fileMode)) { - ulong oldSize = doParseOldMuxState ? (ulong)fileStream.Length : 0uL; - int videoTrackIndex = GetVideoTrackIndex(trackEntries, 0); - bool isComplete = false; - bool isContinuable = false; - ulong lastOutOfs = 0; - ulong segmentOffset = 0; // Will be overwritten below. - IList cuePoints = null; - ulong minStartTime = 0; - ulong timePosition = ulong.MaxValue; - byte[] prefix = null; // Well be overwritten below. - if (doParseOldMuxState && oldSize > 0) { - Console.WriteLine("Trying to use the old mux state to continue downloading."); - prefix = new byte[4096]; - int prefixSize = fileStream.Read(prefix, 0, prefix.Length); - ParsedMuxState parsedMuxState = ParseMuxState( - oldMuxState, oldSize, prefix, prefixSize, videoTrackIndex, trackEntries.Count); - if (parsedMuxState.isComplete) { - Console.WriteLine("The .mkv file is already fully downloaded."); - isComplete = true; - // TODO: Don't even temporarily modify the .muxstate file. - muxStateWriter.WriteRaw(oldMuxState, 0, oldMuxState.Length); - } else if (parsedMuxState.isContinuable) { - Console.WriteLine("Continuing the .mkv file download."); - lastOutOfs = parsedMuxState.lastOutOfs; - segmentOffset = parsedMuxState.vS; - cuePoints = parsedMuxState.cuePoints; - minStartTime = parsedMuxState.vA; - timePosition = parsedMuxState.lastC; - // We may save memory after this by trucating prefix to durationOffset + 4 -- but we don't care. - muxStateWriter.WriteRaw(oldMuxState, 0, parsedMuxState.endOffset); - mediaDataSource.StartChunks(new StateChunkStartTimeReceiver( - muxStateWriter, parsedMuxState.trackChunkStartTimes)); - for (int i = 0; i < trackEntries.Count; ++i) { - // Skip downloading most of the chunk files already in the .mkv (up to lastOutOfs). - mediaDataSource.ConsumeBlocksUntil(i, parsedMuxState.trackLastStartTimes[i]); - } - isContinuable = true; - } else { - Console.WriteLine("Could not use old mux state: " + parsedMuxState); - } - } - if (!isComplete) { - fileStream.SetLength((long)lastOutOfs); - fileStream.Seek((long)lastOutOfs, 0); - if (!isContinuable) { // Not continuing from previous state, writing an .mkv from scratch. - // EBML: http://matroska.org/technical/specs/rfc/index.html - // http://matroska.org/technical/specs/index.html - prefix = GetEbmlHeaderBytes(); - segmentOffset = (ulong)prefix.Length + 12; - muxStateWriter.WriteUlong('X', MUX_STATE_VERSION); // Unique ID and version number. - muxStateWriter.WriteUlong('S', segmentOffset); // About 52. - prefix = Utils.CombineBytes(prefix, GetSegmentBytes( - /*duration:*/maxTrackEndTimeHint, - INITIAL_MEDIA_END_OFFSET_MS, INITIAL_SEEK_HEAD_OFFSET_MS, INITIAL_CUES_OFFSET_MS, - timeScale, trackEntries, isDeterministic)); - fileStream.Write(prefix, 0, prefix.Length); // Write the MKV header. - fileStream.Flush(); - muxStateWriter.WriteBytes('H', prefix); // About 405 bytes long. - muxStateWriter.WriteUlong('V', (ulong)videoTrackIndex); - cuePoints = new List(); - mediaDataSource.StartChunks(new StateChunkStartTimeReceiver( - muxStateWriter, new IList[trackEntries.Count])); - } - ulong seekHeadOffsetMS; // Will be set by WriteClustersAndCues below. - ulong cuesOffsetMS; // Will be set by WriteClustersAndCues below. - WriteClustersAndCues( - fileStream, segmentOffset, videoTrackIndex, GetIsAmsCodecs(trackEntries), - mediaDataSource, muxStateWriter, cuePoints, ref minStartTime, timePosition, - out seekHeadOffsetMS, out cuesOffsetMS); - fileStream.Flush(); - // Update the MKV header with the file size. - ulong mediaEndOffset = (ulong)fileStream.Position; - muxStateWriter.WriteUlong('M', mediaEndOffset); - // Usually this seek position is 45. - ulong maxTrackEndTime = 0; // TODO: mkvmerge calculates this differently (<0.5s -- rounding?) - for (int i = 0; i < mediaDataSource.GetTrackCount(); ++i) { - ulong trackEndTime = mediaDataSource.GetTrackEndTime(i); - if (maxTrackEndTime < trackEndTime) maxTrackEndTime = trackEndTime; - } - // Update the ID.Segment size and ID.Duration with their final values. - int seekOffset = (int)segmentOffset - 7; - // We update the final duration and some offsets in the .mkv header so mplayer (and possibly other - // media players) will be able to seek in the file without additional tricks. More specifically: - // - // play-before play-after seek-before seek-after - // mplayer yes yes no yes - // mplayer -idx yes yes yes yes - // mplayer2 yes yes yes yes - // mplayer2 -idx yes yes yes yes - // VLC 1.0.x no no no no - // VLC 1.1.x no no no no - // VLC 2.0.x ? yes ? yes - // SMPlayer 0.6.9 ? yes ? yes - // - // Legend: - // - // * mplayer: MPlayer SVN-r1.0~rc3+svn20090426-4.4.3 on Ubuntu Lucid - // * mplayer2: MPlayer2 2.0 from http://ftp.mplayer2.org/pub/release/ , mtime 2011-03-26 - // * -idx; The -idx command-line flag of mplayer and mplayer2. - // * play: playing the video sequentially from beginning to end - // * seek: jumping back and forth within the video upon user keypress (e.g. the key), - // including jumping to regions of the .mkv which haven't been downloaded when playback started - // * before: before running UpdatePrefix below, i.e. while the .mkv is being downloaded - // * after: after running UpdatePrefix below - // - // VLC 1.0.x and VLC 1.1.x problems: audio is fine, but the video is jumping back and forth fraction of - // a second. - int updateOffset = UpdatePrefix( - prefix, prefix.Length, segmentOffset, - mediaEndOffset - segmentOffset, - /*seekHeadOffsetMS:*/seekHeadOffsetMS, - /*cuesOffsetMS:*/cuesOffsetMS, - /*duration:*/maxTrackEndTime - minStartTime, timeScale); - fileStream.Seek(seekOffset, 0); - fileStream.Write(prefix, seekOffset, updateOffset - seekOffset); - fileStream.Flush(); - muxStateWriter.WriteUlong('Z', 1); - muxStateWriter.Flush(); - } - } - } - } -} diff --git a/Mp4.cs b/Mp4.cs deleted file mode 100644 index d186134..0000000 --- a/Mp4.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; -namespace Smoothget.Mp4 { - // The 4 bytes (MSB first) of these values correspond to the 4 bytes of the name (e.g. 'f', 't', 'y', p' for ftyp). - public enum ID : uint { - uuid = 1970628964u, - sdtp = 1935963248u, - moof = 1836019558u, - mfhd = 1835427940u, - traf = 1953653094u, - tfhd = 1952868452u, - trun = 1953658222u, - mdat = 1835295092u, - unsupported = 0u, - tfrf = 1u, - tfxd = 2u, - min = 100u, - } - - public class Fragment { - public MovieFragmentBox moof; - public MediaDataBox mdat; - public Fragment(byte[] boxBytes, int start, int end) { - while (start < end) { - Box box = Mp4Utils.GetBox(boxBytes, ref start, end); - if (box == null) { - } else if (box.ID == ID.mdat) { - this.mdat = (box as MediaDataBox); - } else if (box.ID == ID.moof) { - this.moof = (box as MovieFragmentBox); - } - } - } - } - - public class Box { - public ID ID; - public Box(ID id) { - this.ID = id; - } - } - - public class MediaDataBox : Box { - public int Start; - public MediaDataBox(byte[] boxBytes, int start, int end) : base(ID.mdat) { - this.Start = start; - } - } - - public class MovieFragmentHeaderBox : Box { - public uint SequenceNumber; - public MovieFragmentHeaderBox(byte[] boxBytes, int start, int end) : base(ID.mfhd) { - if (end - start != 8) { - throw new Exception("Invalid '" + base.ID + "' length!"); - } - start += 4; - this.SequenceNumber = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - } - - public class MovieFragmentBox : Box { - public MovieFragmentHeaderBox mfhd; - public TrackFragmentBox traf; - public MovieFragmentBox(byte[] boxBytes, int start, int end) : base(ID.moof) { - while (start < end) { - Box box = Mp4Utils.GetBox(boxBytes, ref start, end); - if (box == null) { - } else if (box.ID == ID.traf) { - this.traf = (box as TrackFragmentBox); - } else if (box.ID == ID.mfhd) { - this.mfhd = (box as MovieFragmentHeaderBox); - } - } - } - } - - public class TrackFragmentBox : Box { - public TrackFragmentHeaderBox tfhd; - public TrackRunBox trun; - public SampleDependencyTypeBox sdtp; - public TfrfBox tfrf; - public TfxdBox tfxd; - public TrackFragmentBox(byte[] boxBytes, int start, int end) : base(ID.traf) { - while (start < end) { - Box box = Mp4Utils.GetBox(boxBytes, ref start, end); - if (box != null) { - ID iD = box.ID; - if (iD == ID.tfhd) { - this.tfhd = (box as TrackFragmentHeaderBox); - } else if (iD == ID.sdtp) { - this.sdtp = (box as SampleDependencyTypeBox); - } else if (iD == ID.trun) { - this.trun = (box as TrackRunBox); - } else if (iD == ID.tfrf) { - this.tfrf = (box as TfrfBox); - } else if (iD == ID.tfxd) { - this.tfxd = (box as TfxdBox); - } - } - } - } - } - - public class TfxdBox : Box { - public byte Version; - public byte[] Flags; - public ulong FragmentAbsoluteTime; - public ulong FragmentDuration; - public TfxdBox(byte[] boxBytes, int start, int end) : base(ID.tfxd) { - // TODO: Don't read and populate unused fields. - this.Version = Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; - this.Flags = Mp4Utils.ReadReverseBytes(boxBytes, 3, ref start, end); - if (this.Version == 0) { - this.FragmentAbsoluteTime = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes( - boxBytes, 4, ref start, end), 0); - this.FragmentDuration = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } else { - if (this.Version != 1) { - throw new Exception("Invalid TfxdBox version '" + this.Version + "'!"); - } - this.FragmentAbsoluteTime = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); - this.FragmentDuration = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); - } - } - } - - public class SampleDependencyTypeBox : Box { - public class Element { - public byte reserved; - public byte sample_depends_on; - public byte sample_is_depended_on; - public byte sample_has_redundancy; - public Element(byte reserved, byte sample_depends_on, byte sample_is_depended_on, byte sample_has_redundancy) { - this.reserved = reserved; - this.sample_depends_on = sample_depends_on; - this.sample_is_depended_on = sample_is_depended_on; - this.sample_has_redundancy = sample_has_redundancy; - } - } - public uint version; - public SampleDependencyTypeBox.Element[] array; - public SampleDependencyTypeBox(byte[] boxBytes, int start, int end) : base(ID.sdtp) { - this.version = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - int count = end - start; - this.array = new SampleDependencyTypeBox.Element[count]; - for (int i = 0; i < count; i++) { - byte b = Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; - byte reserved = (byte)(b >> 6); - b <<= 2; - byte sample_depends_on = (byte)(b >> 6); - b <<= 2; - byte sample_is_depended_on = (byte)(b >> 6); - b <<= 2; - byte sample_has_redundancy = (byte)(b >> 6); - this.array[i] = new SampleDependencyTypeBox.Element( - reserved, sample_depends_on, sample_is_depended_on, sample_has_redundancy); - } - } - } - - public class TfrfBox : Box { - public class Element { - public ulong FragmentAbsoluteTime; - public ulong FragmentDuration; - public Element(byte[] boxBytes, byte version, ref int start, int end) { - if (version == 0) { - this.FragmentAbsoluteTime = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - this.FragmentDuration = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } else { - if (version != 1) { - throw new Exception("Invalid TfrfBox version '" + version + "'!"); - } - this.FragmentAbsoluteTime = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); - this.FragmentDuration = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); - } - } - } - public byte Version; - public byte[] Flags; - public Element[] Array; - public TfrfBox(byte[] boxBytes, int start, int end) : base(ID.tfrf) { - this.Version = Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; - this.Flags = Mp4Utils.ReadReverseBytes(boxBytes, 3, ref start, end); - int fragmentCount = (int)Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; - this.Array = new TfrfBox.Element[fragmentCount]; - for (int i = 0; i < fragmentCount; i++) { - this.Array[i] = new TfrfBox.Element(boxBytes, this.Version, ref start, end); - } - // TODO: Do we want to test start == end (in other classes as well?). - } - } - - public class TrackFragmentHeaderBox : Box { - public uint tf_flags; - public uint track_ID; - public ulong base_data_offset; - public uint sample_description_index; - public uint default_sample_duration; - public uint default_sample_size; - public uint default_sample_flags; - public bool base_data_offset_present; - public bool sample_description_index_present; - public bool default_sample_duration_present; - public bool default_sample_size_present; - public bool default_sample_flags_present; - public bool duration_is_empty; - public TrackFragmentHeaderBox(byte[] boxBytes, int start, int end) : base(ID.tfhd) { - // TODO: Don't store field this.tr_flags etc. if not used. - this.tf_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - this.base_data_offset_present = ((1u & this.tf_flags) != 0u); - this.sample_description_index_present = ((2u & this.tf_flags) != 0u); - this.default_sample_duration_present = ((8u & this.tf_flags) != 0u); - this.default_sample_size_present = ((16u & this.tf_flags) != 0u); - this.default_sample_flags_present = ((32u & this.tf_flags) != 0u); - this.duration_is_empty = ((65536u & this.tf_flags) != 0u); - this.track_ID = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - if (this.base_data_offset_present) { - this.base_data_offset = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); - } - if (this.sample_description_index_present) { - this.sample_description_index = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes( - boxBytes, 4, ref start, end), 0); - } - if (this.default_sample_duration_present) { - this.default_sample_duration = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes( - boxBytes, 4, ref start, end), 0); - } - if (this.default_sample_size_present) { - this.default_sample_size = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - if (this.default_sample_flags_present) { - this.default_sample_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - } - } - - public class TrackRunBox : Box { - public struct Element { - public uint sample_duration; - public uint sample_size; - public uint sample_flags; - public uint sample_composition_time_offset; - public Element(uint sample_duration, uint sample_size, uint sample_flags, uint sample_composition_time_offset) { - this.sample_duration = sample_duration; - this.sample_size = sample_size; - this.sample_flags = sample_flags; - this.sample_composition_time_offset = sample_composition_time_offset; - } - } - public uint tr_flags; - public uint sample_count; - public int data_offset; - public uint first_sample_flags; - public TrackRunBox.Element[] array; - public bool data_offset_present; - public bool first_sample_flags_present; - public bool sample_duration_present; - public bool sample_size_present; - public bool sample_flags_present; - public bool sample_composition_time_offsets_present; - public TrackRunBox(byte[] boxBytes, int start, int end) : base(ID.trun) { - // TODO: Don't store field this.tr_flags etc. if not used. - this.tr_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - this.data_offset_present = ((1u & this.tr_flags) != 0u); - this.first_sample_flags_present = ((4u & this.tr_flags) != 0u); - this.sample_duration_present = ((256u & this.tr_flags) != 0u); - this.sample_size_present = ((512u & this.tr_flags) != 0u); - this.sample_flags_present = ((1024u & this.tr_flags) != 0u); - this.sample_composition_time_offsets_present = ((2048u & this.tr_flags) != 0u); - this.sample_count = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - if (this.data_offset_present) { - this.data_offset = BitConverter.ToInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - if (this.first_sample_flags_present) { - this.first_sample_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - this.array = new TrackRunBox.Element[this.sample_count]; - for (int i = 0; i < this.array.Length; i++) { - uint sample_duration = 0u; - uint sample_size = 0u; - uint sample_flags = 0u; - uint sample_composition_time_offset = 0u; - if (this.sample_duration_present) { - sample_duration = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - if (this.sample_size_present) { - sample_size = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - if (this.sample_flags_present) { - sample_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - if (this.sample_composition_time_offsets_present) { - sample_composition_time_offset = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); - } - this.array[i] = new TrackRunBox.Element(sample_duration, sample_size, sample_flags, sample_composition_time_offset); - } - } - } - - internal class Mp4Utils { - public static byte[] ReadReverseBytes(byte[] boxBytes, int count, ref int start, int end) { - if (start + count > end) { - throw new Exception("Short read, wanted " + count); - } - byte[] array = new byte[count]; - Buffer.BlockCopy(boxBytes, start, array, 0, count); - start += count; - Array.Reverse(array); - return array; - } - public static Box GetBox(byte[] boxBytes, ref int start, int end) { - // TODO: Integrate ReadReverseBytes and BitConverter.ToUint*. - int length = BitConverter.ToInt32(ReadReverseBytes(boxBytes, 4, ref start, end), 0); - uint idNum = BitConverter.ToUInt32(ReadReverseBytes(boxBytes, 4, ref start, end), 0); - if (length == 1) { - // TODO: Test this. - length = (int)BitConverter.ToUInt64(ReadReverseBytes(boxBytes, 8, ref start, end), 0) - 16; - } else if (length == 0) { - length = end - start; // TODO: `offset' seems to be correct. Test this. - } else { - length -= 8; - } - if (length < 0) { - throw new Exception("Length too small."); - } - int contentStart = start; - start += length; - if (start > end) { - throw new Exception("Box '" + idNum + "' ends outside the file!"); - } - switch (idNum) { - case (uint)ID.mdat: { return new MediaDataBox(boxBytes, contentStart, start); } - case (uint)ID.mfhd: { return new MovieFragmentHeaderBox(boxBytes, contentStart, start); } - case (uint)ID.moof: { return new MovieFragmentBox(boxBytes, contentStart, start); } - case (uint)ID.sdtp: { return new SampleDependencyTypeBox(boxBytes, contentStart, start); } - case (uint)ID.tfhd: { return new TrackFragmentHeaderBox(boxBytes, contentStart, start); } - case (uint)ID.traf: { return new TrackFragmentBox(boxBytes, contentStart, start); } - case (uint)ID.trun: { return new TrackRunBox(boxBytes, contentStart, start); } - case (uint)ID.uuid: { - Guid uUID = new Guid(ReadReverseBytes(boxBytes, 8, ref contentStart, start)); - if (uUID == TfxdGuid) { - return new TfxdBox(boxBytes, contentStart, start); - } else if (uUID == TfrfGuid) { - return new TfrfBox(boxBytes, contentStart, start); - } else if (uUID == PiffGuid) { - throw new Exception("DRM protected data!"); - } - break; - } - } - return null; - } - private static readonly Guid TfxdGuid = new Guid("6D1D9B05-42D5-44E6-80E2-141DAFF757B2"); - private static readonly Guid TfrfGuid = new Guid("D4807EF2-CA39-4695-8E54-26CB9E46A79F"); - // mp4parser.boxes.piff.PiffSampleEncryptionBox - private static readonly Guid PiffGuid = new Guid("A2394F52-5A9B-4F14-A244-6C427C648DF4"); - } -} diff --git a/Program.cs b/Program.cs deleted file mode 100644 index ec76ae6..0000000 --- a/Program.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Smoothget.Download; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; - -[assembly: AssemblyVersion("3.0.0.3")] - -namespace Smoothget { - internal interface IUrlProcessor { - // Processes inputUrls sequentially, appending the results to outputUrls. - void Process(IList inputUrls, IList outputUrls); - int GetOrder(); - } - - internal class ManifestUrlProcessor : IUrlProcessor { - /*implements*/ public void Process(IList inputUrls, IList outputUrls) { - foreach (string url in inputUrls) { - if (url.EndsWith("/manifest") || url.EndsWith("/Manifest")) { - outputUrls.Add(url.Substring(0, url.Length - 9)); // "/Manifest".Length == 9. - } else { - outputUrls.Add(url); - } - } - } - /*implements*/ public int GetOrder() { return 999; } - } - - internal class MainClass { - private static void Main(string[] args) { - Logo(); - int j; - bool isDeterministic = false; - for (j = 0; j < args.Length; ++j) { - if (args[j] == "--") { - ++j; - break; - } else if (args[j].Length == 0 || args[j] == "-" || args[j][0] != '-') { - break; - } else if (args[j] == "--det") { - isDeterministic = true; - } - } - if (args.Length < j + 2) { - Help(); - } - int lastIdx = args.Length - 1; - string downloadDirectory = args[lastIdx].Trim(Path.GetInvalidFileNameChars()).Trim(Path.GetInvalidPathChars()); - Console.WriteLine("Download directory: " + downloadDirectory); - string[] urls = new string[lastIdx - j]; - for (int i = j; i < lastIdx; ++i) { - urls[i - j] = args[i]; - } - IList partUrls = ProcessUrls(urls); - Console.WriteLine("Parts to download:"); - for (int i = 0; i < partUrls.Count; i++) { - Console.WriteLine(" Part URL: " + partUrls[i]); - } - Console.WriteLine(); - for (int i = 0; i < partUrls.Count; i++) { - RecordAndMux(partUrls[i], downloadDirectory, isDeterministic); - } - Console.WriteLine("All downloading and muxing done."); - } - - // May return the same reference (urls). - private static IList ProcessUrls(IList urls) { - Type urlProcessorType = typeof(IUrlProcessor); - List urlProcessors = new List(); - foreach (Type type in urlProcessorType.Assembly.GetTypes()) { - if (type.IsClass && urlProcessorType.IsAssignableFrom(type)) { - urlProcessors.Add((IUrlProcessor)type.GetConstructor(Type.EmptyTypes).Invoke(null)); - } - } - urlProcessors.Sort(CompareUrlProcessorOrder); - foreach (IUrlProcessor urlProcessor in urlProcessors) { - List nextUrls = new List(); - urlProcessor.Process(urls, nextUrls); - urls = nextUrls; - } - return urls; - } - - private static int CompareUrlProcessorOrder(IUrlProcessor a, IUrlProcessor b) { - return a.GetOrder().CompareTo(b.GetOrder()); - } - - // A thin wrapper for callbacks of duration progress reporting and stopping. - private class MuxingInteractiveState { - private bool hasDisplayedDuration; - private ulong startTicks; - private ulong reachedBaseTicks; - private Thread thread; - private IStoppable stoppable; - public MuxingInteractiveState() { - this.hasDisplayedDuration = false; - this.startTicks = 0; - } - public void SetupStop(bool isLive, IStoppable stoppable) { - if (this.thread != null) { - throw new Exception("ASSERT: Unexpected thread."); - } - if (isLive) { - this.stoppable = stoppable; - Console.WriteLine("Press any key to stop recording!"); - this.thread = new Thread(new ThreadStart(StopRecoding)); - this.thread.Start(); - } - } - public void Abort() { - if (this.thread != null) this.thread.Abort(); - } - public void StopRecoding() { // Runs in a separate thread parallel with DownloadAndMux. - Console.ReadKey(true); - this.stoppable.Stop(); - } - public void DisplayDuration(ulong reachedTicks, ulong totalTicks) { - if (!this.hasDisplayedDuration) { - this.hasDisplayedDuration = true; - Console.Error.WriteLine("Recording duration:"); - } - ulong eta = 0; - if (reachedTicks > 0) { - ulong nowTicks = (ulong)DateTime.Now.Ticks; - if (this.startTicks == 0) { - this.startTicks = nowTicks; - this.reachedBaseTicks = reachedTicks; - } else { - // TODO: Improve the ETA calculation, it seems to be jumping up and down. - // Here nowTicks and this.startTicks are measured in real time. - // Here totalTicks, reachedTicks and this.reachedBaseTicks are measured in video - // timecode. - double etaDouble = (nowTicks - this.startTicks + 0.0) * - (totalTicks - reachedTicks) / (reachedTicks - this.reachedBaseTicks); - if (etaDouble > 0.0 && etaDouble < 3.6e12) eta = (ulong)etaDouble; // < 100 hours. - } - } - // TODO: Use a StringBuilder. - string msg = "\r" + new TimeSpan((long)(reachedTicks - reachedTicks % 10000000)); - if (eta != 0) { - msg += ", ETA " + new TimeSpan((long)(eta - eta % 10000000)); // Round down to whole seconds. - } else { - msg += " "; // Clear the end (ETA) of the previously displayed message. - } - Console.Write(msg); - } - } - private static void RecordAndMux(string ismFileName, string outputDirectory, bool isDeterministic) { - string mkvPath = outputDirectory + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(ismFileName) + ".mkv"; - string muxStatePath = Path.ChangeExtension(mkvPath, "muxstate"); - if (File.Exists(mkvPath) && !File.Exists(muxStatePath)) { - Console.WriteLine("Already downloaded MKV: " + mkvPath); - Console.WriteLine(); - return; - } - Console.WriteLine("Will mux to MKV: " + mkvPath); - string manifestUrl = ismFileName + "/manifest"; - Uri manifestUri; - string manifestPath; - if (manifestUrl.StartsWith("http://") || manifestUrl.StartsWith("https://")) { - manifestUri = new Uri(manifestUrl); - manifestPath = null; - } else if (manifestUrl.StartsWith("file://")) { - // Uri.LocalPath converts %20 back to a space etc. (unlike Uri.AbsolutePath). - // TODO: Does Uri.LocalPath work on Windows (drive letters, / to \ etc.)? - manifestUri = null; - manifestPath = new Uri(manifestUrl).LocalPath; - } else { - manifestUri = null; - manifestPath = manifestUrl; - } - DateTime now = DateTime.Now; - MuxingInteractiveState muxingInteractiveState = new MuxingInteractiveState(); - Downloader.DownloadAndMux(manifestUri, manifestPath, mkvPath, isDeterministic, - new TimeSpan(10, 0, 0), // 10 hours, 0 minutes, 0 seconds for live streams. - muxingInteractiveState.SetupStop, - muxingInteractiveState.DisplayDuration); - muxingInteractiveState.Abort(); - Console.Error.WriteLine(); - Console.WriteLine("Downloading finished in " + DateTime.Now.Subtract(now).ToString()); - } - private static void Logo() { - AssemblyName name = Assembly.GetEntryAssembly().GetName(); - Console.WriteLine(string.Concat(new object[] { name.Name, " v", name.Version })); - Console.WriteLine(); - } - private static void Help() { - // Console.WriteLine(" Soda Media Center"); // TODO: Where does it come crom? - AssemblyName name = Assembly.GetEntryAssembly().GetName(); - Console.WriteLine("Microsoft IIS Smooth Streaming downloader and muxer to MKV."); - Console.WriteLine(); - Console.WriteLine("Supported media stream formats:"); // TODO: Really only these? - Console.WriteLine("- Audio: AAC, WMA"); - Console.WriteLine("- Video: H264, VC-1"); - Console.WriteLine(); - Console.WriteLine("Usage:"); - Console.WriteLine(name.Name + " [ ...] [...] "); - Console.WriteLine(" is an .ism (or /manifest) file or URL."); - Console.WriteLine(" can be just . , a properly named file will be created."); - Console.WriteLine("Many temporary files and subdirs may be created and left in ."); - Console.WriteLine("--det Enable deterministic MKV output (no random, no current time)."); - Environment.Exit(1); - } - } -} diff --git a/README.md b/README.md index b989e48..de437f9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ smoothget: Microsoft IIS Smooth Streaming downloader and muxer to MKV Compilation on Ubuntu and Debian: $ sudo apt-get install git-core mono-gmcs - $ git clone http://github.com/pinglossy/smoothget.git - $ cd smoothget - $ ./c.sh + $ git clone + $ cd smoothget/compile-mono + $ ./compile.sh $ mono smoothget.exe + +Compilation on Windows: + Install Visual Studio 2013 and open compile-visualstudio/smoothget.sln \ No newline at end of file diff --git a/Smooth.cs b/Smooth.cs deleted file mode 100644 index c969fb5..0000000 --- a/Smooth.cs +++ /dev/null @@ -1,676 +0,0 @@ -using Smoothget; -using Smoothget.Mkv; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -namespace Smoothget.Smooth { - public enum MediaStreamType { - Audio, - Video, - Script - } - - internal class ManifestInfo { - public uint MajorVersion; - public uint MinorVersion; - public ulong Duration; - public bool IsLive; - public ulong TimeScale; - public Uri Uri; - public IDictionary Attributes; - public IList AvailableStreams; - public IList SelectedStreams; - public ulong TotalTicks; - private ManifestInfo(XmlNode element, Uri uri) { - this.Uri = uri; - if (element.Name != "SmoothStreamingMedia") { - throw new Exception("Source element is not a(n) SmoothStreamingMedia element!"); - } - this.Attributes = Parse.Attributes(element); - this.MajorVersion = Parse.UInt32Attribute(this.Attributes, "MajorVersion"); - this.MinorVersion = Parse.UInt32Attribute(this.Attributes, "MinorVersion"); - this.Duration = Parse.UInt64Attribute(this.Attributes, "Duration"); - this.IsLive = false; - if (this.Attributes.ContainsKey("IsLive")) { - this.IsLive = Parse.BoolAttribute(this.Attributes, "IsLive"); - } - this.TimeScale = 10000000uL; - if (this.Attributes.ContainsKey("TimeScale")) { - this.TimeScale = Parse.UInt64Attribute(this.Attributes, "TimeScale"); - } - this.AvailableStreams = new List(); - foreach (XmlNode element2 in element.SelectNodes("StreamIndex")) { // Automatic cast to XmlNode. - this.AvailableStreams.Add(new StreamInfo(element2, this.Uri)); - } - this.SelectedStreams = new List(); - for (int i = 0; i < this.AvailableStreams.Count; i++) { - if (this.AvailableStreams[i].Type != MediaStreamType.Script) { - this.SelectedStreams.Add(this.AvailableStreams[i]); - } - } - this.TotalTicks = this.IsLive ? 0 : (ulong)(this.Duration / this.TimeScale * 10000000.0); - } - public static ManifestInfo ParseManifest(Stream manifestStream, Uri manifestUri) { - XmlDocument xmlDocument = new XmlDocument(); - xmlDocument.Load(manifestStream); - XmlNode xmlNode = xmlDocument.SelectSingleNode("SmoothStreamingMedia"); - if (xmlNode == null) { - throw new Exception(string.Format("Manifest root element must be <{0}>!", "SmoothStreamingMedia")); - } - return new ManifestInfo(xmlNode, manifestUri); - } - public string GetDescription() { - string text = ""; // TODO: Use something like a StringBuilder to avoid O(n^2) concatenation. - text += "Manifest:\n"; - text += " Duration: "; - if (this.IsLive) { - text += "LIVE\n"; - } - else { - text += new TimeSpan((long)this.TotalTicks) + "\n"; - } - for (int i = 0; i < this.AvailableStreams.Count; i++) { - object obj = text; - text = string.Concat(new object[] { obj, " Stream ", i + 1, ": ", this.AvailableStreams[i].Type, "\n" }); - switch (this.AvailableStreams[i].Type) { - case MediaStreamType.Audio: - case MediaStreamType.Video: { - foreach (TrackInfo trackInfo in this.AvailableStreams[i].AvailableTracks) { - text += " " + trackInfo.Description; - if (this.AvailableStreams[i].SelectedTracks.Contains(trackInfo)) { - text += " [selected]"; - } - text += "\n"; - } - break; - } - case MediaStreamType.Script: { - text += " Script ignored.\n"; - break; - } - default: { - text += " WARNING: Unsupported track of type " + this.AvailableStreams[i].Type + "\n"; - break; - } - } - } - return text; - } - } - - internal class StreamInfo { - private string pureUrl; - public IDictionary Attributes; - public IDictionary CustomAttributes; - public IList AvailableTracks; - public IList SelectedTracks; - public IList ChunkList; - public string Subtype; - public MediaStreamType Type; - public int ChunkCount; - public StreamInfo(XmlNode element, Uri manifestUri) { - if (element.Name != "StreamIndex") { - throw new Exception("Source element is not a(n) StreamIndex element!"); - } - this.Attributes = Parse.Attributes(element); - this.CustomAttributes = Parse.CustomAttributes(element); - this.Type = Parse.MediaStreamTypeAttribute(this.Attributes, "Type"); - this.Subtype = this.Attributes.ContainsKey("Subtype") ? Parse.StringAttribute(this.Attributes, "Subtype") : ""; - if (this.Attributes.ContainsKey("Url")) { - this.CheckUrlAttribute(); - } - this.AvailableTracks = new List(); - XmlNodeList xmlNodeList = element.SelectNodes("QualityLevel"); - int i; - for (i = 0; i < xmlNodeList.Count; ++i) { - TrackInfo trackInfo; - if (this.Type == MediaStreamType.Audio) { - trackInfo = new AudioTrackInfo(xmlNodeList[i], this.Attributes, (uint)i, this); - } else if (this.Type == MediaStreamType.Video) { - trackInfo = new VideoTrackInfo(xmlNodeList[i], this.Attributes, (uint)i, this); - } else { - continue; - } - int num = 0; - while (num < this.AvailableTracks.Count && this.AvailableTracks[num].Bitrate > trackInfo.Bitrate) { - num++; - } - this.AvailableTracks.Insert(num, trackInfo); - } - this.ChunkList = new List(); - XmlNodeList xmlNodeList2 = element.SelectNodes("c"); - ulong num2 = 0uL; - for (i = 0; i < xmlNodeList2.Count; i++) { - ChunkInfo chunkInfo = new ChunkInfo(xmlNodeList2[i], (uint)i, num2); - this.ChunkList.Add(chunkInfo); - num2 += chunkInfo.Duration; - } - if (this.Attributes.ContainsKey("Chunks")) { - uint chunkCount = Parse.UInt32Attribute(this.Attributes, "Chunks"); - if (this.ChunkList.Count > 0 && this.ChunkList.Count != chunkCount) { - throw new Exception("Chunk count mismatch: c=" + this.ChunkList.Count + " chunks=" + chunkCount); - } - this.ChunkCount = (int)chunkCount; - } else { - this.ChunkCount = this.ChunkList.Count; // Can be 0 if no `(); - if (this.AvailableTracks.Count > 0) { - this.SelectedTracks.Add(this.AvailableTracks[0]); - } - } - private void CheckUrlAttribute() { - string text = Parse.StringAttribute(this.Attributes, "Url"); - string[] array = text.Split(new char[] { - '/' - }); - if (array.Length != 2) { - throw new Exception("Invalid UrlPattern!"); - } - string text2 = array[0]; - string text3 = array[1]; - array = text2.Split(new char[] { - '(', - ')' - }); - if (array.Length != 3 || array[2].Length != 0) { - throw new Exception("Invalid QualityLevelsPattern!"); - } - if (array[0] != "QualityLevels") { - throw new Exception("Invalid QualityLevelsNoun!"); - } - string text4 = array[1]; - array = text4.Split(new char[] { - ',' - }); - if (array.Length > 2) { - throw new Exception("Invalid QualityLevelsPredicatePattern!"); - } - if (array[0] != "{bitrate}" && array[0] != "{Bitrate}") { - throw new Exception("Missing BitrateSubstitution!"); - } - if (array.Length == 2 && array[1] != "{CustomAttributes}") { - throw new Exception("Missing CustomAttributesSubstitution!"); - } - array = text3.Split(new char[] { - '(', - ')' - }); - if (array.Length != 3 || array[2].Length != 0) { - throw new Exception("Invalid FragmentsPattern!"); - } - if (array[0] != "Fragments") { - throw new Exception("Invalid FragmentsNoun!"); - } - string text5 = array[1]; - array = text5.Split(new char[] { - '=' - }); - if (array.Length != 2) { - throw new Exception("Invalid FragmentsPatternPredicate!"); - } - if (this.Attributes.ContainsKey("Name")) { - if (array[0] != Parse.StringAttribute(this.Attributes, "Name")) { - throw new Exception("Missing TrackName!"); - } - } - else { - if (array[0] != Parse.StringAttribute(this.Attributes, "Type")) { - throw new Exception("Missing TrackName!"); - } - } - if (array[1] != "{start time}" && array[1] != "{start_time}") { - throw new Exception("Missing StartTimeSubstitution!"); - } - } - public string GetChunkUrl(uint bitrate, ulong startTime) { - return this.pureUrl + "/" + Parse.StringAttribute(this.Attributes, "Url") - .Replace("{bitrate}", bitrate.ToString()) - .Replace("{Bitrate}", bitrate.ToString()) - .Replace("{start time}", startTime.ToString()) - .Replace("{start_time}", startTime.ToString()); - } - } - - internal class TrackInfo { - public IDictionary Attributes; - public uint Bitrate; - public IDictionary CustomAttributes; - public uint Index; - public StreamInfo Stream; - public string Description; - public TrackEntry TrackEntry; - public TrackInfo(XmlNode element, uint index, StreamInfo stream) { - if (element.Name != "QualityLevel") { - throw new Exception("Source element is not a(n) QualityLevel element!"); - } - this.Attributes = Parse.Attributes(element); - this.CustomAttributes = Parse.CustomAttributes(element); - this.Index = index; - if (this.Attributes.ContainsKey("Index")) { - this.Index = Parse.UInt32Attribute(this.Attributes, "Index"); - } - if (this.Index != index) { - throw new Exception("Missing quality level index: " + index); - } - this.Bitrate = Parse.UInt32Attribute(this.Attributes, "Bitrate"); - this.Stream = stream; - } - } - - internal class ChunkInfo { - public uint Index; - public ulong Duration; - public ulong StartTime; - public IDictionary Attributes; - public ChunkInfo(XmlNode element, uint index, ulong starttime) { - if (element.Name != "c") { - throw new Exception("Source element is not a(n) c element!"); - } - this.Attributes = Parse.Attributes(element); - this.Index = index; - if (this.Attributes.ContainsKey("n")) { - this.Index = Parse.UInt32Attribute(this.Attributes, "n"); - } - if (this.Index != index) { - throw new Exception("Missing chunk index: " + index); - } - this.StartTime = starttime; - if (this.Attributes.ContainsKey("t")) { - this.StartTime = Parse.UInt64Attribute(this.Attributes, "t"); - } - if (this.Attributes.ContainsKey("d")) { - this.Duration = Parse.UInt64Attribute(this.Attributes, "d"); - } - } - } - - internal class AudioTrackInfo : TrackInfo { - private static string GetCodecNameForAudioTag(ushort audioTag) { - switch (audioTag) { - case 1: { return "LPCM"; } - case 85: { return "MP3"; } - case 255: case 5633: { return "AAC"; } - case 353: { return "WMA2"; } - case 354: { return "WMAP"; } - case 65534: { return "Vendor-extensible format"; } - default: { throw new Exception("Unsupported AudioTag '" + audioTag + "'!"); } - } - } - private class WaveFormatEx { - public ushort wFormatTag; - public ushort nChannels; - public uint nSamplesPerSec; - public uint nAvgBytesPerSec; - public ushort nBlockAlign; - public ushort wBitsPerSample; - public byte[] DecoderSpecificData; // Max size is 65535 bytes. - public WaveFormatEx(byte[] data) { - if (data == null || data.Length < 18) { - throw new Exception("Invalid WaveFormatEx data!"); - } - ushort num = BitConverter.ToUInt16(data, 16); - if (data.Length != (int)(18 + num)) { - throw new Exception("Invalid cbSize value!"); - } - this.wFormatTag = BitConverter.ToUInt16(data, 0); - this.nChannels = BitConverter.ToUInt16(data, 2); - this.nSamplesPerSec = (uint)BitConverter.ToUInt16(data, 4); - this.nAvgBytesPerSec = (uint)BitConverter.ToUInt16(data, 8); - this.nBlockAlign = BitConverter.ToUInt16(data, 12); - this.wBitsPerSample = BitConverter.ToUInt16(data, 14); - this.DecoderSpecificData = new byte[(int)num]; - Buffer.BlockCopy(data, 18, this.DecoderSpecificData, 0, this.DecoderSpecificData.Length); - } - public WaveFormatEx(ushort wFormatTag, ushort nChannels, uint nSamplesPerSec, uint nAvgBytesPerSec, ushort nBlockAlign, - ushort wBitsPerSample, byte[] DecoderSpecificData) { - if (DecoderSpecificData != null && DecoderSpecificData.Length > 65535) { - throw new Exception("DecoderSpecificData too long."); - } - this.wFormatTag = wFormatTag; - this.nChannels = nChannels; - this.nSamplesPerSec = nSamplesPerSec; - this.nAvgBytesPerSec = nAvgBytesPerSec; - this.nBlockAlign = nBlockAlign; - this.wBitsPerSample = wBitsPerSample; - this.DecoderSpecificData = DecoderSpecificData; - } - public byte[] GetBytes() { - byte[] array = new byte[18 + this.DecoderSpecificData.Length]; - Buffer.BlockCopy(BitConverter.GetBytes(this.wFormatTag), 0, array, 0, 2); - Buffer.BlockCopy(BitConverter.GetBytes(this.nChannels), 0, array, 2, 2); - Buffer.BlockCopy(BitConverter.GetBytes(this.nSamplesPerSec), 0, array, 4, 4); - Buffer.BlockCopy(BitConverter.GetBytes(this.nAvgBytesPerSec), 0, array, 8, 4); - Buffer.BlockCopy(BitConverter.GetBytes(this.nBlockAlign), 0, array, 12, 2); - Buffer.BlockCopy(BitConverter.GetBytes(this.wBitsPerSample), 0, array, 14, 2); - Buffer.BlockCopy(BitConverter.GetBytes((ushort)this.DecoderSpecificData.Length), 0, array, 16, 2); - if (array.Length != 18) { - Buffer.BlockCopy(this.DecoderSpecificData, 0, array, 18, this.DecoderSpecificData.Length); - } - return array; - } - } - private static readonly uint[] MP4_SamplingRate = new uint[] { - 96000u, 88200u, 64000u, 48000u, 44100u, 32000u, 24000u, 22050u, 16000u, 12000u, 11025u, 8000u, 7350u, 0u, 0u, 0u }; - private static readonly string MP4_Channels = "\x00\x01\x02\x03\x04\x05\x06\x08"; - private static byte[] GetAudioSpecificConfigBytes(uint samplingRate, byte numberOfChannels) { - // public enum Profile : byte { MAIN = 1, LC, SSR, LTP, SBR,Scalable } - // ushort num = (ushort)((ushort)profile << 11); - ushort num = (ushort)((ushort)2 << 11); // Profile.LC. - int num2 = 0; - while (MP4_SamplingRate[num2] != samplingRate && num2 < MP4_SamplingRate.Length) { - num2++; - } - if (num2 > MP4_SamplingRate.Length) { - throw new Exception("Invalid sampling rate!"); - } - num += (ushort)((ushort)num2 << 7); - num2 = 0; - while (MP4_Channels[num2] != numberOfChannels && num2 < MP4_Channels.Length) { - num2++; - } - if (num2 > MP4_Channels.Length) { - throw new Exception("Invalid number of channels!"); - } - num += (ushort)((ushort)num2 << 3); - return Utils.InplaceReverseBytes(BitConverter.GetBytes(num)); - } - - public AudioTrackInfo(XmlNode element, IDictionary streamAttributes, uint index, StreamInfo stream) : base(element, index, stream) { - WaveFormatEx waveFormatEx; - if (base.Attributes.ContainsKey("WaveFormatEx")) { - byte[] data = Parse.HexStringAttribute(base.Attributes, "WaveFormatEx"); - waveFormatEx = new WaveFormatEx(data); - } - else { - ushort wFormatTag = Parse.UInt16Attribute(base.Attributes, "AudioTag"); - ushort nChannels = Parse.UInt16Attribute(base.Attributes, "Channels"); - uint nSamplesPerSec = Parse.UInt32Attribute(base.Attributes, "SamplingRate"); - uint num = Parse.UInt32Attribute(base.Attributes, "Bitrate"); - ushort nBlockAlign = Parse.UInt16Attribute(base.Attributes, "PacketSize"); - ushort wBitsPerSample = Parse.UInt16Attribute(base.Attributes, "BitsPerSample"); - byte[] decoderSpecificData = Parse.HexStringAttribute(base.Attributes, "CodecPrivateData"); - waveFormatEx = new WaveFormatEx(wFormatTag, nChannels, nSamplesPerSec, num / 8u, nBlockAlign, wBitsPerSample, decoderSpecificData); - } - byte[] audioInfoBytes = MkvUtils.GetAudioInfoBytes( - waveFormatEx.nSamplesPerSec, (ulong)waveFormatEx.nChannels, (ulong)waveFormatEx.wBitsPerSample); - switch (waveFormatEx.wFormatTag) { - case 353: case 354: { - base.TrackEntry = new TrackEntry(TrackType.Audio, audioInfoBytes, CodecID.A_MS, waveFormatEx.GetBytes()); - break; - } - case 255: case 5633: { - base.TrackEntry = new TrackEntry(TrackType.Audio, audioInfoBytes, CodecID.A_AAC, GetAudioSpecificConfigBytes( - waveFormatEx.nSamplesPerSec, (byte)waveFormatEx.nChannels)); - break; - } - case 1: { - throw new Exception("Unsupported audio format: 'LPCM'!"); - } - case 65534: { - throw new Exception("Unsupported audio format: 'Vendor-extensible format'!"); - } - default: { - throw new Exception("Unsupported AudioTag: '" + waveFormatEx.wFormatTag + "'"); - } - } - if (base.Attributes.ContainsKey("Name")) { - base.TrackEntry.Name = Parse.StringAttribute(streamAttributes, "Name"); - } - base.TrackEntry.Language = LanguageID.Hungarian; // TODO: Make this configurable. - base.Description = string.Format("{0} {1} channels {2} Hz @ {3} kbps", new object[] { - GetCodecNameForAudioTag(waveFormatEx.wFormatTag), waveFormatEx.nChannels, waveFormatEx.nSamplesPerSec, - base.Bitrate / 1000u }); - } - } - - internal class VideoTrackInfo : TrackInfo { - private static byte[] GetBitmapInfoHeaderBytes(int biWidth, int biHeight, ushort biPlanes, ushort biBitCount, - uint biCompression, uint biSizeImage, int biXPelsPerMeter, int biYPelsPerMeter, - uint biClrUsed, uint biClrImportant, byte[] codecPrivateData) { - int biSize = 40 + codecPrivateData.Length; - byte[] array = new byte[biSize]; - Buffer.BlockCopy(BitConverter.GetBytes(biSize), 0, array, 0, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biWidth), 0, array, 4, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biHeight), 0, array, 8, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biPlanes), 0, array, 12, 2); - Buffer.BlockCopy(BitConverter.GetBytes(biBitCount), 0, array, 14, 2); - Buffer.BlockCopy(BitConverter.GetBytes(biCompression), 0, array, 16, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biSizeImage), 0, array, 20, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biXPelsPerMeter), 0, array, 24, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biYPelsPerMeter), 0, array, 28, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biClrUsed), 0, array, 32, 4); - Buffer.BlockCopy(BitConverter.GetBytes(biClrImportant), 0, array, 36, 4); - Buffer.BlockCopy(codecPrivateData, 0, array, 40, codecPrivateData.Length); - return array; - } - public VideoTrackInfo(XmlNode element, IDictionary streamAttributes, uint index, StreamInfo stream) - : base(element, index, stream) { - uint pixelWidth = base.Attributes.ContainsKey("MaxWidth") ? Parse.UInt32Attribute(base.Attributes, "MaxWidth") : - base.Attributes.ContainsKey("Width") ? Parse.UInt32Attribute(base.Attributes, "Width") : - streamAttributes.ContainsKey("MaxWidth") ? Parse.UInt32Attribute(streamAttributes, "MaxWidth") : 0u; - if (pixelWidth == 0u) { - throw new Exception("Missing video width attribute!"); - } - uint pixelHeight = base.Attributes.ContainsKey("MaxHeight") ? Parse.UInt32Attribute(base.Attributes, "MaxHeight") : - base.Attributes.ContainsKey("Height") ? Parse.UInt32Attribute(base.Attributes, "Height") : - streamAttributes.ContainsKey("MaxHeight") ? Parse.UInt32Attribute(streamAttributes, "MaxHeight") : 0u; - if (pixelHeight == 0u) { - throw new Exception("Missing video height attribute!"); - } - uint displayWidth = streamAttributes.ContainsKey("DisplayWidth") ? - Parse.UInt32Attribute(streamAttributes, "DisplayWidth") : 0u; - if (displayWidth == 0u) { - displayWidth = pixelWidth; - } - uint displayHeight = streamAttributes.ContainsKey("DisplayHeight") ? - Parse.UInt32Attribute(streamAttributes, "DisplayHeight") : 0u; - if (displayHeight == 0u) { - displayHeight = pixelHeight; - } - byte[] videoInfoBytes = MkvUtils.GetVideoInfoBytes( - (ulong)pixelWidth, (ulong)pixelHeight, (ulong)displayWidth, (ulong)displayHeight); - byte[] codecPrivateData = base.Attributes.ContainsKey("CodecPrivateData") ? - Parse.HexStringAttribute(base.Attributes, "CodecPrivateData") : null; - if (codecPrivateData == null) { - throw new Exception("Missing CodecPrivateData attribute!"); - } - string fourcc = base.Attributes.ContainsKey("FourCC") ? Parse.StringAttribute(base.Attributes, "FourCC") : - streamAttributes.ContainsKey("FourCC") ? Parse.StringAttribute(streamAttributes, "FourCC") : null; - switch (fourcc) { - case "WVC1": { - base.TrackEntry = new TrackEntry( - TrackType.Video, videoInfoBytes, CodecID.V_MS, VideoTrackInfo.GetVfWCodecPrivate( - pixelWidth, pixelHeight, fourcc, codecPrivateData)); - break; - } - case "H264": { - ushort nalUnitLengthField = 4; - if (base.Attributes.ContainsKey("NALUnitLengthField")) { - nalUnitLengthField = Parse.UInt16Attribute(base.Attributes, "NALUnitLengthField"); - } - base.TrackEntry = new TrackEntry( - TrackType.Video, videoInfoBytes, CodecID.V_AVC, - GetAVCCodecPrivate(codecPrivateData, nalUnitLengthField)); - break; - } - case null: { - throw new Exception("Missing FourCC attribute!"); - } - default: { - throw new Exception("Unsupported video FourCC: '" + fourcc + "'"); - } - } - if (base.Attributes.ContainsKey("Name")) { - base.TrackEntry.Name = Parse.StringAttribute(streamAttributes, "Name"); - } - base.TrackEntry.Language = LanguageID.Hungarian; // TODO: Make this configurable. - base.Description = string.Format("{0} {1}x{2} ({3}x{4}) @ {5} kbps", new object[] { - fourcc, pixelWidth, pixelHeight, displayWidth, displayHeight, base.Bitrate / 1000u }); - } - private static byte[] GetAVCCodecPrivate(byte[] codecPrivateData, ushort nalUnitLengthField) { - switch (nalUnitLengthField) { - case 1: case 2: case 4: { - string text = Utils.HexEncodeString(codecPrivateData); - if (string.IsNullOrEmpty(text)) { - throw new Exception("Invalid AVC1 attribute: CodecPrivateData"); - } - string[] array = text.Split(new string[] { "00000001" }, 0); - if (array.Length != 3) { - throw new Exception("Invalid AVC1 attribute: CodecPrivateData"); - } - byte[] array2 = Utils.HexDecodeString(array[1]); - if (array2 == null || array2.Length < 3) { - throw new Exception("Invalid SPS in CodecPrivateData!"); - } - byte[] array3 = Utils.HexDecodeString(array[2]); - if (array3 == null) { - throw new Exception("Invalid PPS in CodecPrivateData!"); - } - return GetAVCDecoderConfigurationBytes( - array2[1], array2[2], array2[3], (byte)(nalUnitLengthField - 1), - new byte[1][] { array2 }, new byte[1][] { array3 }); - } - } - throw new Exception("Invalid AVC1 attribute: NALUnitLengthField"); - } - private static byte[] GetAVCDecoderConfigurationBytes( - byte AVCProfileIndication, byte profile_compatibility, byte AVCLevelIndication, - byte lengthSizeMinusOne, byte[][] sequenceParameterSetNALUnits, byte[][] pictureParameterSetNALUnits) { - if (lengthSizeMinusOne != 0 && lengthSizeMinusOne != 1 && lengthSizeMinusOne != 3) { - throw new Exception("Invalid lengthSizeMinusOne value in AVCDecoderConfigurationRecord!"); - } - if (sequenceParameterSetNALUnits.Length > 31) { - throw new Exception("Invalid sequenceParameterSetNALUnits count in AVCDecoderConfigurationRecord!"); - } - if (pictureParameterSetNALUnits.Length > 255) { - throw new Exception("Invalid pictureParameterSetNALUnits count in AVCDecoderConfigurationRecord!"); - } - int i = 7; - int limitS = sequenceParameterSetNALUnits.Length; - for (int b = 0; b < limitS; ++b) { - i += 2 + sequenceParameterSetNALUnits[b].Length; - } - int limitP = pictureParameterSetNALUnits.Length; - for (int b = 0; b < limitP; ++b) { - i += 2 + pictureParameterSetNALUnits[b].Length; - } - byte[] array = new byte[i]; - array[0] = 1; // configurationVersion. - array[1] = AVCProfileIndication; - array[2] = profile_compatibility; - array[3] = AVCLevelIndication; - array[4] = (byte)(252 ^ lengthSizeMinusOne); - array[5] = (byte)(224 ^ limitS); - i = 6; - for (int b = 0; b < limitS; ++b) { - int size = sequenceParameterSetNALUnits[b].Length; - array[i] = (byte)(size >> 8); - array[i + 1] = (byte)(size & 255); - i += 2; - Buffer.BlockCopy(sequenceParameterSetNALUnits[b], 0, array, i, size); - i += size; - } - array[i++] = (byte)limitP; - for (int b = 0; b < limitP; ++b) { - int size = pictureParameterSetNALUnits[b].Length; - array[i] = (byte)(size >> 8); - array[i + 1] = (byte)(size & 255); - i += 2; - Buffer.BlockCopy(pictureParameterSetNALUnits[b], 0, array, i, size); - i += size; - } - return array; - } - private static byte[] GetVfWCodecPrivate(uint width, uint height, string fourCC, byte[] codecPrivateData) { - if (width > 2147483647u) { // int.MaxValue - throw new Exception("Invalid video width value!"); - } - if (height > 2147483647u) { // int.MaxValue - throw new Exception("Invalid video height value!"); - } - if (fourCC.Length != 4) { - throw new Exception("Invalid video FourCC value!"); - } - return GetBitmapInfoHeaderBytes( - (int)width, (int)height, 1, 24, - /*biCompression:*/BitConverter.ToUInt32(Encoding.ASCII.GetBytes(fourCC), 0), - width * height * 24u / 8u, 0, 0, 0u, 0u, codecPrivateData); - } - } - - internal class Parse { - public static IDictionary Attributes(XmlNode element) { - Dictionary dictionary = new Dictionary(); - foreach (XmlAttribute xmlAttribute in element.Attributes) { - dictionary.Add(xmlAttribute.Name, xmlAttribute.Value); - } - return dictionary; - } - public static IDictionary CustomAttributes(XmlNode element) { - Dictionary dictionary = new Dictionary(); - XmlNode xmlNode = element.SelectSingleNode("CustomAttributes"); - if (xmlNode != null) { - foreach (XmlNode xmlNode2 in xmlNode.SelectNodes("Attribute")) { - dictionary.Add(xmlNode2.Attributes.GetNamedItem("Name").Value, xmlNode2.Attributes.GetNamedItem("Value").Value); - } - } - return dictionary; - } - public static string StringAttribute(IDictionary attributes, string key) { - if (!attributes.ContainsKey(key)) { - throw new Exception(key + " key is missing!"); - } - return attributes[key]; - } - public static bool BoolAttribute(IDictionary attributes, string key) { - string text = Parse.StringAttribute(attributes, key); - bool result; - if (!bool.TryParse(text, out result)) { - throw new Exception("Cannot parse the " + key + " key!"); - } - return result; - } - public static ushort UInt16Attribute(IDictionary attributes, string key) { - string text = Parse.StringAttribute(attributes, key); - ushort result; - if (!ushort.TryParse(text, out result)) { - throw new Exception("Cannot parse the " + key + " key!"); - } - return result; - } - public static uint UInt32Attribute(IDictionary attributes, string key) { - string text = Parse.StringAttribute(attributes, key); - uint result; - if (!uint.TryParse(text, out result)) { - throw new Exception("Cannot parse the " + key + " key!"); - } - return result; - } - public static ulong UInt64Attribute(IDictionary attributes, string key) { - string text = Parse.StringAttribute(attributes, key); - ulong result; - if (!ulong.TryParse(text, out result)) { - throw new Exception("Cannot parse the " + key + " key!"); - } - return result; - } - public static byte[] HexStringAttribute(IDictionary attributes, string key) { - return Utils.HexDecodeString(Parse.StringAttribute(attributes, key)); - } - public static MediaStreamType MediaStreamTypeAttribute(IDictionary attributes, string key) { - switch (Parse.StringAttribute(attributes, key)) { - case "video": { return MediaStreamType.Video; } - case "audio": { return MediaStreamType.Audio; } - case "text": { return MediaStreamType.Script; } - default: { throw new Exception("Cannot parse the " + key + " key!"); } - } - } - } -} diff --git a/Utils.cs b/Utils.cs deleted file mode 100644 index b6c1acd..0000000 --- a/Utils.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -namespace Smoothget { - public class Utils { - public static byte[] InplaceReverseBytes(byte[] a) { - Array.Reverse(a); - return a; - } - public static byte[] CombineByteArrays(IList arrays) { - int totalSize = 0; - for (int i = 0; i < arrays.Count; ++i) { - totalSize += arrays[i].Length; - } - byte[] array = new byte[totalSize]; - int pos = 0; - for (int i = 0; i < arrays.Count; ++i) { - Buffer.BlockCopy(arrays[i], 0, array, pos, arrays[i].Length); - pos += arrays[i].Length; - } - return array; - } - public static byte[] CombineByteArraysAndArraySegments(IList arrays, IList> arraySegments) { - int totalSize = 0; - if (arrays != null) { - for (int i = 0; i < arrays.Count; ++i) { - totalSize += arrays[i].Length; - } - } - for (int i = 0; i < arraySegments.Count; ++i) { - totalSize += arraySegments[i].Count; - } - byte[] array = new byte[totalSize]; - int pos = 0; - if (arrays != null) { - for (int i = 0; i < arrays.Count; ++i) { - Buffer.BlockCopy(arrays[i], 0, array, pos, arrays[i].Length); - pos += arrays[i].Length; - } - } - for (int i = 0; i < arraySegments.Count; ++i) { - Buffer.BlockCopy(arraySegments[i].Array, arraySegments[i].Offset, array, pos, arraySegments[i].Count); - pos += arraySegments[i].Count; - } - return array; - } - public static byte[] CombineBytes(byte[] a, byte[] b) { - byte[] array = new byte[a.Length + b.Length]; - Buffer.BlockCopy(a, 0, array, 0, a.Length); - Buffer.BlockCopy(b, 0, array, a.Length, b.Length); - return array; - } - public static byte[] CombineBytes(byte[] a, byte[] b, byte[] c) { - byte[] array = new byte[a.Length + b.Length + c.Length]; - Buffer.BlockCopy(a, 0, array, 0, a.Length); - Buffer.BlockCopy(b, 0, array, a.Length, b.Length); - Buffer.BlockCopy(c, 0, array, a.Length + b.Length, c.Length); - return array; - } - public static string HexEncodeString(byte[] bytes) { - // This is simple and fast, but the complicated implementation below is faster, - // http://stackoverflow.com/questions/623104/c-sharp-byte-to-hex-string - // return BitConverter.ToString(input).Replace("-", ""); - char[] c = new char[bytes.Length << 1]; - byte b; - for(int bx = 0, cx = 0; bx < bytes.Length; ++bx) { - b = ((byte)(bytes[bx] >> 4)); - c[cx++] = (char)(b > 9 ? b + 0x57 : b + 0x30); - b = ((byte)(bytes[bx] & 0x0F)); - c[cx++]=(char)(b > 9 ? b + 0x57 : b + 0x30); - } - return new string(c); - } - public static byte[] HexDecodeString(string hexEncodedData) { - // This is a fast implementation from http://stackoverflow.com/questions/623104/c-sharp-byte-to-hex-string - if (hexEncodedData == null) return null; - byte[] buffer = new byte[hexEncodedData.Length >> 1]; - char c; - for (int bx = 0, sx = 0; bx < buffer.Length; ++bx) { - c = hexEncodedData[sx++]; - if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; - buffer[bx] = (byte)((c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')) << 4); - c = hexEncodedData[sx++]; - if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; - buffer[bx] |= (byte)(c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')); - } - return buffer; - } - // TODO: Save code size by using Encoding.ASCII and HexEncodeString. - public static byte[] HexDecodeBytes(byte[] hexEncodedData, int start, int end) { - // This is a fast implementation based on http://stackoverflow.com/questions/623104/c-sharp-byte-to-hex-string - if (hexEncodedData == null) return null; - if (start < 0) start = 0; - if (end >= hexEncodedData.Length) end = hexEncodedData.Length; - if (start >= end) return new byte[] {}; - if (((end - start) & 1) != 0) return null; - byte[] buffer = new byte[(end - start) >> 1]; - byte c; - for (int bx = 0, sx = start; bx < buffer.Length; ++bx) { - c = hexEncodedData[sx++]; - if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; - buffer[bx] = (byte)((c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')) << 4); - c = hexEncodedData[sx++]; - if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; - buffer[bx] |= (byte)(c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')); - } - return buffer; - } - public static string EscapeString(string s) { - // This is simpler and less bloated than the CSharpCodeProvider in - // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal - // TODO: Save memory by doing less concatenations. - return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; - } - // Always makes a copy. - public static byte[] GetSubBytes(byte[] bytes, int start, int end) { - if (bytes == null) return null; - if (start < 0) start = 0; - if (end >= bytes.Length) end = bytes.Length; - if (start >= end) return new byte[] {}; - byte[] subBytes = new byte[end - start]; - Buffer.BlockCopy(bytes, start, subBytes, 0, end - start); - return subBytes; - } - public static bool ArePrefixBytesEqual(byte[] a, byte[] b, int size) { - for (int i = 0; i < size; ++i) { - if (a[i] != b[i]) return false; - } - return true; - } - } -} diff --git a/c.sh b/compile-mono/compile.sh old mode 100755 new mode 100644 similarity index 83% rename from c.sh rename to compile-mono/compile.sh index 3317fc0..005079f --- a/c.sh +++ b/compile-mono/compile.sh @@ -12,4 +12,4 @@ set -ex gmcs -out:smoothget.exe -debug- -optimize+ -langversion:ISO-2 \ - Download.cs Mkv.cs Mp4.cs Program.cs Smooth.cs Utils.cs + ../src/Download.cs ../src/Mkv.cs ../src/Mp4.cs ../src/Program.cs ../src/Smooth.cs ../src/Utils.cs diff --git a/compile-visualstudio/App.config b/compile-visualstudio/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/compile-visualstudio/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/compile-visualstudio/Properties/AssemblyInfo.cs b/compile-visualstudio/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..02de1f8 --- /dev/null +++ b/compile-visualstudio/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("smoothget")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("smoothget")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5fa27d6e-6aed-4893-9ff5-aa7981cb3cfc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/compile-visualstudio/smoothget.csproj b/compile-visualstudio/smoothget.csproj new file mode 100644 index 0000000..7eec541 --- /dev/null +++ b/compile-visualstudio/smoothget.csproj @@ -0,0 +1,75 @@ + + + + + Debug + AnyCPU + {F76FF4A3-468F-46FC-9807-8CF5AFAC3542} + Exe + Properties + smoothget + smoothget + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + Download.cs + + + Mkv.cs + + + Mp4.cs + + + Program.cs + + + Smooth.cs + + + Utils.cs + + + + + + + + + \ No newline at end of file diff --git a/compile-visualstudio/smoothget.sln b/compile-visualstudio/smoothget.sln new file mode 100644 index 0000000..29295d6 --- /dev/null +++ b/compile-visualstudio/smoothget.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2013 for Windows Desktop +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "smoothget", "smoothget.csproj", "{F76FF4A3-468F-46FC-9807-8CF5AFAC3542}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F76FF4A3-468F-46FC-9807-8CF5AFAC3542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F76FF4A3-468F-46FC-9807-8CF5AFAC3542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F76FF4A3-468F-46FC-9807-8CF5AFAC3542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F76FF4A3-468F-46FC-9807-8CF5AFAC3542}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Download.cs b/src/Download.cs new file mode 100644 index 0000000..0f73bb5 --- /dev/null +++ b/src/Download.cs @@ -0,0 +1,596 @@ +using Smoothget; +using Smoothget.Mkv; +using Smoothget.Mp4; +using Smoothget.Smooth; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +namespace Smoothget.Download +{ + // Download progress for a single track. + // This is a pure data class, please don't add logic. + // Cannot use a `struct' here, because its fields are read-only once the constructor returns. + internal class Track + { + public TrackInfo TrackInfo; + public ulong NextStartTime; // Start time of the next chunk to download. + public int DownloadedChunkCount; + public Track(TrackInfo trackInfo) + { + this.TrackInfo = trackInfo; + this.DownloadedChunkCount = 0; + } + } + + // This is a pure data class, please don't add logic. + // Don't convert MediaSample to a struct, it makes the executable 512 bytes larger. + internal class MediaSample + { + public long Offset; // In bytes. + public ulong StartTime; + public int Length; // In bytes. + public bool IsKeyFrame; + public MediaSample(long offset, int length, ulong startTime, bool isKeyFrame) + { + this.Offset = offset; + this.Length = length; + this.StartTime = startTime; + this.IsKeyFrame = isKeyFrame; + } + } + + public interface IStoppable + { + void Stop(); + } + + public delegate void SetupStop(bool isLive, IStoppable stoppable); + public delegate void DisplayDuration(ulong reachedTicks, ulong totalTicks); + + // To be passed to WriteMkv when isCombo == true. + internal class DownloadingMediaDataSource : IMediaDataSource, IStoppable + { + // Usually Tracks has 2 elements: one for the video track and one for the audio track. + private IList Tracks; + private string ManifestParentPath; + private ulong MinStartTime; + private ulong TotalDuration; + private ulong TimeScale; + private DisplayDuration DisplayDuration; + + // Usually TrackSamples has 2 elements: one for the video track and one for the audio track. + private IList[] TrackSamples; + private int[] TrackSampleStartIndexes; + // this.TrackFirstBytes[i] corresponds to this.TrackSamples[i][this.TrackSampleStartIndexes[i]].GetBytes(), + // or null if not converted yet. + private MediaDataBlock[] TrackFirstBlocks; + // this.TrackFirstFileDatas[i] contains the whole file for this.TrackSamples[i][this.TrackSampleStartIndexes[i]] or + // it's null. + private byte[][] TrackFirstFileDatas; + private IChunkStartTimeReceiver ChunkStartTimeReceiver; + private bool IsLive; + public volatile bool IsStopped; + private ulong StopAfter; + private ulong TotalTicks; // For ETA calculation. + + // The object created may modify trackSamples in a destructive way, to save memory. + // Expects tracks[...].NextStartTime and tracks[...].DownloadedChunkCount to be initialized. + public DownloadingMediaDataSource(IList tracks, string manifestParentPath, + ulong timeScale, bool isLive, ulong stopAfter, ulong totalTicks, + DisplayDuration displayDuration) + { + int trackCount = tracks.Count; + this.Tracks = tracks; + this.ManifestParentPath = manifestParentPath; + this.TimeScale = timeScale; + this.DisplayDuration = displayDuration; + this.IsLive = isLive; + this.StopAfter = stopAfter; + this.TotalDuration = 0; + this.MinStartTime = ulong.MaxValue; + this.IsStopped = false; + for (int i = 0; i < trackCount; ++i) + { + ulong chunkStartTime = tracks[i].NextStartTime; + if (this.MinStartTime > chunkStartTime) + { + this.MinStartTime = chunkStartTime; + } + } + this.TotalTicks = totalTicks; + this.TrackSamples = new IList[trackCount]; // Items initialized to null. + for (int i = 0; i < trackCount; ++i) + { + this.TrackSamples[i] = new List(); + } + this.TrackSampleStartIndexes = new int[trackCount]; // Items initialized to 0. + this.TrackFirstBlocks = new MediaDataBlock[trackCount]; // Items initialized to null. + this.TrackFirstFileDatas = new byte[trackCount][]; // Items initialized to null. + this.ChunkStartTimeReceiver = null; + } + + /*implements IStoppable*/ + public void Stop() + { + this.IsStopped = true; // TODO: Is `volatile' enough to make this thread-safe? + } + + /*implements*/ + public int GetTrackCount() + { + return this.TrackFirstBlocks.Length; + } + + /*implements*/ + public ulong GetTrackEndTime(int trackIndex) + { + return this.Tracks[trackIndex].NextStartTime; + } + + /*implements*/ + public void StartChunks(IChunkStartTimeReceiver chunkStartTimeReceiver) + { + this.ChunkStartTimeReceiver = chunkStartTimeReceiver; + for (int trackIndex = 0; trackIndex < this.Tracks.Count; ++trackIndex) + { + // Propagate the start time of the verify first chunk of the track. If resuming from a .muxstate, + // this also checks that the .muxstate is consistent with what we want to do. + // TODO: If not consistent, don't use the .muxstate instead of making the process abort. + this.ChunkStartTimeReceiver.SetChunkStartTime( + trackIndex, this.Tracks[trackIndex].DownloadedChunkCount, this.Tracks[trackIndex].NextStartTime); + } + } + + // Returns false on EOF, true on success. + private bool DownloadNextChunk(int trackIndex) + { + IList mediaSamples = this.TrackSamples[trackIndex]; + Track track = this.Tracks[trackIndex]; + if ((track.DownloadedChunkCount >= track.TrackInfo.Stream.ChunkCount && !this.IsLive) || + this.IsStopped) return false; // EOF. + this.TrackSampleStartIndexes[trackIndex] = 0; + mediaSamples.Clear(); + while (mediaSamples.Count == 0) + { // Download next chunk. + int chunkIndex = track.DownloadedChunkCount; + if ((chunkIndex >= track.TrackInfo.Stream.ChunkCount && !this.IsLive) || + this.IsStopped) return false; + ulong chunkStartTime = track.NextStartTime; + if (this.IsLive && chunkStartTime * 1e7 / this.TimeScale >= this.StopAfter) return false; + if (track.TrackInfo.Stream.ChunkList.Count > chunkIndex) + { + ulong chunkStartTimeInList = track.TrackInfo.Stream.ChunkList[chunkIndex].StartTime; + if (chunkStartTime != chunkStartTimeInList) + { + throw new Exception( + "StartTime mismatch in .ism and chunk files: ism=" + chunkStartTimeInList + + " file=" + chunkStartTime + " track=" + trackIndex + " chunk=" + chunkIndex); + } + } + byte[] contents = Downloader.DownloadChunk(track.TrackInfo, mediaSamples, chunkStartTime, this.ManifestParentPath, + this.IsLive, out track.NextStartTime); + if (contents == null) + { + this.IsStopped = true; + // The URL has been printed by DownloadChunk above. + // TODO: Finish muxing the .mkv so the user gets something complete. + throw new Exception("Error downloading chunk " + chunkIndex + " of track " + trackIndex); + } + ++track.DownloadedChunkCount; + if (track.TrackInfo.Stream.ChunkList.Count > chunkIndex) + { + ulong nextStartTimeInList = + chunkStartTime + track.TrackInfo.Stream.ChunkList[chunkIndex].Duration; + if (track.NextStartTime != nextStartTimeInList) + { + throw new Exception( + "next StartTime mismatch in .ism and chunk files: ism=" + nextStartTimeInList + + " file=" + track.NextStartTime + " track=" + trackIndex + + " chunk=" + chunkIndex); + } + } + this.ChunkStartTimeReceiver.SetChunkStartTime( + trackIndex, track.DownloadedChunkCount, track.NextStartTime); + this.TrackFirstFileDatas[trackIndex] = contents; + // Notify the listener of the successful download. + ulong trackTotalDuration = track.NextStartTime - this.MinStartTime; + if (this.TotalDuration < trackTotalDuration) + { + this.TotalDuration = trackTotalDuration; + ulong reachedTicks = (ulong)(this.TotalDuration * 1e7 / this.TimeScale); + this.DisplayDuration(reachedTicks, this.TotalTicks); + } + } + return true; + } + + /*implements*/ + public MediaDataBlock PeekBlock(int trackIndex) + { + if (this.TrackFirstBlocks[trackIndex] != null) return this.TrackFirstBlocks[trackIndex]; + IList mediaSamples = this.TrackSamples[trackIndex]; + int k = this.TrackSampleStartIndexes[trackIndex]; + if (k >= mediaSamples.Count) + { // Finished processing this chunk, download next chunk. + if (!DownloadNextChunk(trackIndex)) return null; + k = 0; + } + + MediaSample mediaSample = mediaSamples[k]; + mediaSamples[k] = null; // Save memory once this function returns. + return this.TrackFirstBlocks[trackIndex] = new MediaDataBlock( + new ArraySegment(this.TrackFirstFileDatas[trackIndex], (int)mediaSample.Offset, mediaSample.Length), + mediaSample.StartTime, mediaSample.IsKeyFrame); + } + + /*implements*/ + public void ConsumeBlock(int trackIndex) + { + if (this.TrackFirstBlocks[trackIndex] == null && this.PeekBlock(trackIndex) == null) + { + throw new Exception("ASSERT: No MediaSample to consume."); + } + this.TrackFirstBlocks[trackIndex] = null; // Save memory and signify for the next call to PeekBlock. + if (++this.TrackSampleStartIndexes[trackIndex] >= this.TrackSamples[trackIndex].Count) + { + this.TrackSamples[trackIndex].Clear(); // Save memory. + this.TrackFirstFileDatas[trackIndex] = null; // Save memory. + } + } + + /*implements:*/ + public void ConsumeBlocksUntil(int trackIndex, ulong startTime) + { + // This is correct for ETA only if ConsumeBlocksUntil is called once. + int k = this.TrackSampleStartIndexes[trackIndex]; + IList mediaSamples = this.TrackSamples[trackIndex]; + int mediaSampleCount = mediaSamples.Count; + if (this.TrackFirstBlocks[trackIndex] != null) + { + // TODO: Test this branch. + if (this.TrackFirstBlocks[trackIndex].StartTime > startTime) return; // Nothing to consume. + this.TrackFirstBlocks[trackIndex] = null; // Save memory and signify for the next call to PeekBlock. + if (k < mediaSampleCount && mediaSamples[k].StartTime > startTime) + { + throw new Exception("ASSERT: Inconsistent TrackFirstBlocks and TrackSamples."); + } + } + Track track = this.Tracks[trackIndex]; + // GetChunkStartTime returns ulong.MaxValue if the chunk index is too large for it. Good. It shouldn't happen + // though, because the next start time after track.DownloadedChunkCount is always available. + if (k < mediaSampleCount && + this.ChunkStartTimeReceiver.GetChunkStartTime(trackIndex, track.DownloadedChunkCount) > startTime) + { + // We may find where to stop within the current chunk (mediaSamples). + // TODO: Test this branch. + for (; k < mediaSampleCount; ++k) + { + if (mediaSamples[k].StartTime > startTime) + { + this.TrackSampleStartIndexes[trackIndex] = k; + return; + } + } + } + // Consumed the whole mediaSamples. + mediaSamples.Clear(); // Save memory. + this.TrackFirstFileDatas[trackIndex] = null; // Save memory. + this.TrackSampleStartIndexes[trackIndex] = 0; + + // Consume chunks which start too early. This step makes resuming downloads very fast, because there are whole + // chunk files which we don't have to download again. + // + // GetChunkStartTime returns ulong.MaxValue if the chunk index is too large for it. Good. + // + // We could use track.TrackInfo.Stream.ChunkList here to consume more in this loop, but by design we don't rely + // on ChunkList. Using ChunkList here wouldn't make resuming previous downloads (using .muxstate) faster, + // because GetChunkStartTime provides the necessary speedup for that. + // + // TODO: Do a binary search. (The effect of this optimization would be negligible.) + ulong nextNextChunkStartTime; + while ((nextNextChunkStartTime = this.ChunkStartTimeReceiver.GetChunkStartTime( + trackIndex, track.DownloadedChunkCount + 1)) <= startTime) + { + // Consume chunk with index track.DownloadedChunkCount. + track.NextStartTime = nextNextChunkStartTime; + ++track.DownloadedChunkCount; + } + + // At this point the next chunk (track.DownloadedChunkCount) starts <= startTime, and the chunk after that + // (track.DownloadedChunkCount + 1) doesn't exist or starts > startTime. So we load the next chunk, and consume + // every mediaSample starting <= startTime in it. That's enough, because there is nothing to consume in further + // chunks (starting with the ``after that'' chunk). We have a `while' loop in case GetChunkStartTime above + // has returned ulong.MaxValue, so we have do download more in order to figure out what to consume. + while (DownloadNextChunk(trackIndex)) + { + mediaSampleCount = mediaSamples.Count; + if (mediaSampleCount == 0) + { + throw new Exception("ASSERT: Expected media samples after download."); + } + for (k = 0; k < mediaSampleCount; ++k) + { + if (mediaSamples[k].StartTime > startTime) + { + this.TrackSampleStartIndexes[trackIndex] = k; + return; + } + } + mediaSamples.Clear(); // Save memory. + } + // Just reached EOF on trackIndex. + this.TrackSamples[trackIndex].Clear(); // Save memory. + this.TrackFirstFileDatas[trackIndex] = null; // Save memory. + } + } + + // TODO: Move most of Downloader outside Downloader. + public class Downloader + { + // Exactly one of manifestUri and manifestPath must be set. + public static void DownloadAndMux(Uri manifestUri, string manifestPath, string mkvPath, bool isDeterministic, TimeSpan stopAfter, + SetupStop setupStop, DisplayDuration displayDuration) + { + string manifestParentPath = null; // A null indicates a remote manifest file. + ManifestInfo manifestInfo; + if (manifestPath != null) + { + manifestParentPath = Path.GetDirectoryName(manifestPath); + Console.WriteLine("Parsing local manifest file: " + manifestPath); + using (FileStream manifestStream = new FileStream(manifestPath, FileMode.Open)) + { + manifestInfo = ManifestInfo.ParseManifest(manifestStream, /*manifestUri:*/new Uri(LOCAL_URL_PREFIX)); + } + } + else + { + Console.WriteLine("Downloading and parsing manifest: " + manifestUri); + WebClient webClient = new WebClient(); + using (Stream manifestStream = webClient.OpenRead(manifestUri)) + { + manifestInfo = ManifestInfo.ParseManifest(manifestStream, manifestUri); + } + } + Console.Write(manifestInfo.GetDescription()); + + IList tracks = new List(); + foreach (StreamInfo streamInfo in manifestInfo.SelectedStreams) + { + foreach (TrackInfo trackInfo in streamInfo.SelectedTracks) + { + tracks.Add(new Track(trackInfo)); + } + } + IList trackEntries = new List(); + IList> trackSamples = new List>(); + for (int i = 0; i < tracks.Count; ++i) + { + trackEntries.Add(tracks[i].TrackInfo.TrackEntry); + trackEntries[i].TrackNumber = (ulong)(i + 1); + trackSamples.Add(new List()); + } + for (int i = 0; i < tracks.Count; i++) + { + // TODO: Add a facility to start live streams from a later chunk (it was chunkIndex=10 previously). + // Our design allows for an empty ChunkList, in case live streams are growing. + tracks[i].NextStartTime = tracks[i].TrackInfo.Stream.ChunkList.Count == 0 ? 0 : + tracks[i].TrackInfo.Stream.ChunkList[0].StartTime; + } + // TODO: Test for live streams (see the StackOverflow question). + Console.WriteLine("Also muxing selected tracks to MKV: " + mkvPath); + try + { + if (Directory.GetParent(mkvPath) != null && + !Directory.GetParent(mkvPath).Exists) + Directory.GetParent(mkvPath).Create(); + } + catch (IOException) + { + // TODO: Add nicer error reporting, without a stack trace. + throw new Exception("Cannot not create the directory of .mkv: " + mkvPath); + } + ulong maxTrackEndTimeHint = manifestInfo.Duration; + for (int i = 0; i < tracks.Count; ++i) + { + IList chunkInfos = tracks[i].TrackInfo.Stream.ChunkList; + int j = chunkInfos.Count - 1; + if (j >= 0) + { // Our design allows for an empty ChunkList. + ulong trackDuration = chunkInfos[j].StartTime + chunkInfos[j].Duration; + if (maxTrackEndTimeHint < trackDuration) maxTrackEndTimeHint = trackDuration; + } + } + // The .muxstate file is approximately 1/5441.43 of the size of the .mkv. + // The .muxstate file is around 28.088 bytes per second. TODO: Update this after n. + // Sometimes totalDuration of video is 1156420602, audio is 1156818141 (larger), so we just take the maximum. + string muxStatePath = Path.ChangeExtension(mkvPath, "muxstate"); + string muxStateOldPath = muxStatePath + ".old"; + byte[] oldMuxState = null; + if (File.Exists(muxStatePath)) + { // False for directories. + using (FileStream fileStream = new FileStream(muxStatePath, FileMode.Open)) + { + oldMuxState = ReadFileStream(fileStream); + } + if (oldMuxState.Length > 0) + { + // File.Move fails with IOException if the destination already exists. + // C# and .NET SUXX: There is no atomic overwrite-move. + try + { + File.Move(muxStatePath, muxStateOldPath); + } + catch (IOException) + { + File.Replace(muxStatePath, muxStateOldPath, null, true); + } + } + } + DownloadingMediaDataSource source = new DownloadingMediaDataSource( + tracks, manifestParentPath, manifestInfo.TimeScale, + manifestInfo.IsLive, (ulong)stopAfter.Ticks, manifestInfo.TotalTicks, displayDuration); + setupStop(manifestInfo.IsLive, source); + MuxStateWriter muxStateWriter = new MuxStateWriter(new FileStream(muxStatePath, FileMode.Create)); + try + { + MkvUtils.WriteMkv(mkvPath, trackEntries, source, maxTrackEndTimeHint, manifestInfo.TimeScale, isDeterministic, + oldMuxState, muxStateWriter); + } + finally + { + muxStateWriter.Close(); + } + File.Delete(muxStatePath); + if (File.Exists(muxStateOldPath)) + { + File.Delete(muxStateOldPath); + } + } + + private static readonly string LOCAL_URL_PREFIX = "http://local/"; + + // Modifies track in place, and appends to mediaSamples. + // Returns null on network failure or empty file, otherwise it returns a non-empty array. + // The chunk file contents are returned, and are not saved to disk. + internal static byte[] DownloadChunk(TrackInfo trackInfo, IList mediaSamples, ulong chunkStartTime, + string manifestParentPath, bool isLive, out ulong nextStartTime) + { + nextStartTime = 0; // Set even if null is returned. + string chunkUrl = trackInfo.Stream.GetChunkUrl(trackInfo.Bitrate, chunkStartTime); + // TODO: Move TrackInfo away from Track, keep only fields necessary here, excluding ChunkList. + byte[] downloadedBytes; // Will be set below. + if (manifestParentPath != null) + { // It was a local manifest, so read the chunk from a local file. + if (!chunkUrl.StartsWith(LOCAL_URL_PREFIX)) + { + throw new Exception("ASSERT: Missing local URL prefix."); + } + // Example chunk URL: "http://local/QualityLevels(900000)/Fragments(video=0)". + // TODO: Maybe this needs some further unescaping of %5A etc. (can be tested locally). + string chunkDownloadedPath = manifestParentPath + Path.DirectorySeparatorChar + + chunkUrl.Substring(LOCAL_URL_PREFIX.Length).Replace('/', Path.DirectorySeparatorChar); + using (FileStream fileStream = new FileStream(chunkDownloadedPath, FileMode.Open)) + { + downloadedBytes = ReadFileStream(fileStream); + } + if (downloadedBytes.Length == 0) + { + Console.WriteLine(); + Console.WriteLine("Local chunk file empty: " + chunkDownloadedPath); + return null; + } + } + else + { // Download from the web. + WebClient webClient = new WebClient(); + try + { + // TODO: What's the timeout on this? + downloadedBytes = webClient.DownloadData(chunkUrl); + } + catch (WebException) + { + Thread.Sleep(isLive ? 4000 : 2000); + try + { + downloadedBytes = webClient.DownloadData(chunkUrl); + } + catch (WebException) + { + Thread.Sleep(isLive ? 6000 : 3000); + try + { + downloadedBytes = webClient.DownloadData(chunkUrl); + } + catch (WebException) + { + // It's an acceptable behavior to stop downloading live streams after 10 seconds. + // If it's really live, there should be a new chunk update available every 10 seconds. + Console.WriteLine(); + Console.WriteLine("Error downloading chunk " + chunkUrl); + return null; + } + } + } + } + if (downloadedBytes.Length == 0) + { + Console.WriteLine(); + Console.WriteLine("Chunk empty: " + chunkUrl); + return null; + } + Fragment fragment = new Fragment(downloadedBytes, 0, downloadedBytes.Length); + // This appends to mediaSamples. + nextStartTime = ParseFragment(fragment, mediaSamples, trackInfo.Stream.Type, chunkStartTime); + if (nextStartTime <= chunkStartTime) + { + throw new Exception("Found empty chunk."); + } + return downloadedBytes; + } + + // TODO: Move this to a generic utility class. + private static byte[] ReadFileStream(FileStream fileStream) + { + int fileSize = (int)fileStream.Length; // TODO: Can this be negative etc. for non-regular files? + if (fileSize <= 0) return new byte[0]; + byte[] array = new byte[fileSize]; + if (fileSize != fileStream.Read(array, 0, fileSize)) + { + throw new Exception("ASSERT: Short read from MediaSample file " + fileStream.Name + ", wanted " + fileSize); + } + if (0 != fileStream.Read(array, 0, 1)) + { + throw new Exception("ASSERT: Long read from MediaSample file " + fileStream.Name + ", wanted " + fileSize); + } + return array; + } + + // Appends to `samples'. + // Returns nextStartTime. + private static ulong ParseFragment(Fragment fragment, IList samples, MediaStreamType type, + ulong chunkStartTime) + { + // A fragment is a ``chunk'' (with a corresponding in its duration) in the ISM manifest file. + TrackFragmentBox traf = fragment.moof.traf; + if (traf.tfxd != null) + { + chunkStartTime = traf.tfxd.FragmentAbsoluteTime; + } + ulong nextStartTime = 0uL; + if (traf.tfrf != null && traf.tfrf.Array.Length > 0u) + { + nextStartTime = traf.tfrf.Array[0].FragmentAbsoluteTime; + } + long sampleOffset = fragment.mdat.Start; + uint defaultSampleSize = traf.tfhd.default_sample_size; + uint sampleSize = defaultSampleSize; + uint defaultSampleDuration = traf.tfhd.default_sample_duration; + uint duration = defaultSampleDuration; + ulong totalDuration = 0; + uint sampleCount = traf.trun.sample_count; + TrackRunBox.Element[] array = defaultSampleSize == 0u || defaultSampleDuration == 0u ? traf.trun.array : null; + for (uint i = 0; i < sampleCount; ++i) + { + if (defaultSampleSize == 0u) + { + sampleSize = array[i].sample_size; + } + if (defaultSampleDuration == 0u) + { + duration = array[i].sample_duration; + } + // We add a few dozen MediaSample entries for a chunk. + samples.Add(new MediaSample(sampleOffset, (int)sampleSize, chunkStartTime, + /*isKeyFrame:*/i == 0 || type == MediaStreamType.Audio)); + chunkStartTime += (ulong)duration; + totalDuration += (ulong)duration; + sampleOffset += sampleSize; + } + return nextStartTime != 0uL ? nextStartTime : chunkStartTime; + } + } +} diff --git a/src/Mkv.cs b/src/Mkv.cs new file mode 100644 index 0000000..36f5b10 --- /dev/null +++ b/src/Mkv.cs @@ -0,0 +1,2286 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +namespace Smoothget.Mkv +{ + public enum CodecID + { + V_AVC, + V_MS, + A_AAC, + A_MS + } + + // Integer values correspond to the return values of GetVIntForTrackType. + public enum TrackType + { + Video = 1, + Audio = 2, + Complex = 3, + Logo = 16, + Subtitle = 17, + Buttons = 18, + Control = 32, + } + + public struct CuePoint + { + private ulong CueTime; + private ulong CueTrack; + public ulong CueClusterPosition; + public CuePoint(ulong cueTime, ulong cueTrack, ulong cueClusterPosition) + { + this.CueTime = cueTime; + this.CueTrack = cueTrack; + this.CueClusterPosition = cueClusterPosition; + } + public byte[] GetBytes() + { + // TODO: Do this with fewer temporary arrays. + return MkvUtils.GetEEBytes(ID.CuePoint, Utils.CombineBytes( + MkvUtils.GetEEBytes(ID.CueTime, MkvUtils.GetVintBytes(this.CueTime)), + MkvUtils.GetEEBytes(ID.CueTrackPositions, Utils.CombineBytes( + MkvUtils.GetEEBytes(ID.CueTrack, MkvUtils.GetVintBytes(this.CueTrack)), + MkvUtils.GetEEBytes(ID.CueClusterPosition, MkvUtils.GetVintBytes(this.CueClusterPosition)))))); + } + } + + // Integer values correspond to GetBytesForID: MkvUtils.GetDataSizeBytes((ulong)id). + public enum ID + { + EBML = 172351395, + EBMLVersion = 646, + EBMLReadVersion = 759, + EBMLMaxIDLength = 754, + EBMLMaxSizeLength = 755, + DocType = 642, + DocTypeVersion = 647, + DocTypeReadVersion = 645, + Void = 108, + Segment = 139690087, + SeekHead = 21863284, + Seek = 3515, + SeekID = 5035, + SeekPosition = 5036, + Info = 88713574, + SegmentUID = 13220, + TimecodeScale = 710577, + Duration = 1161, + DateUTC = 1121, + MuxingApp = 3456, + WritingApp = 5953, + Cluster = 256095861, + Timecode = 103, + SimpleBlock = 35, + Tracks = 106212971, + TrackEntry = 46, + TrackNumber = 87, + TrackUID = 13253, + TrackType = 3, + FlagEnabled = 57, + FlagDefault = 8, + FlagForced = 5546, + FlagLacing = 28, + Name = 4974, + Language = 177564, + CodecID = 6, + CodecPrivate = 9122, + Video = 96, + FlagInterlaced = 26, + PixelWidth = 48, + PixelHeight = 58, + DisplayWidth = 5296, + DisplayHeight = 5306, + Audio = 97, + SamplingFrequency = 53, + Channels = 31, + BitDepth = 8804, + Cues = 206814059, + CuePoint = 59, + CueTime = 51, + CueTrackPositions = 55, + CueTrack = 119, + CueClusterPosition = 113, + } + + // See also TrackEntry.LanguageCodes. + // Please don't change the order of the names here, because TrackEntry.LanguageCodes corresponds to it. + public enum LanguageID + { + Abkhazian, + Achinese, + Acoli, + Adangme, + Adygei, + Adyghe, + Afar, + Afrihili, + Afrikaans, + AfroAsiaticLanguages, + Ainu, + Akan, + Akkadian, + Albanian, + Alemannic, + Aleut, + AlgonquianLanguages, + Alsatian, + AltaicLanguages, + Amharic, + Angika, + ApacheLanguages, + Arabic, + Aragonese, + Arapaho, + Arawak, + Armenian, + Aromanian, + ArtificialLanguages, + Arumanian, + Assamese, + Asturian, + Asturleonese, + AthapascanLanguages, + AustralianLanguages, + AustronesianLanguages, + Avaric, + Avestan, + Awadhi, + Aymara, + Azerbaijani, + Bable, + Balinese, + BalticLanguages, + Baluchi, + Bambara, + BamilekeLanguages, + BandaLanguages, + BantuLanguages, + Basa, + Bashkir, + Basque, + BatakLanguages, + Bedawiyet, + Beja, + Belarusian, + Bemba, + Bengali, + BerberLanguages, + Bhojpuri, + BihariLanguages, + Bikol, + Bilin, + Bini, + Bislama, + Blin, + Bliss, + Blissymbolics, + Blissymbols, + BokmålNorwegian, + Bosnian, + Braj, + Breton, + Buginese, + Bulgarian, + Buriat, + Burmese, + Caddo, + Castilian, + Catalan, + CaucasianLanguages, + Cebuano, + CelticLanguages, + CentralAmericanIndianLanguages, + CentralKhmer, + Chagatai, + ChamicLanguages, + Chamorro, + Chechen, + Cherokee, + Chewa, + Cheyenne, + Chibcha, + Chichewa, + Chinese, + Chinookjargon, + Chipewyan, + Choctaw, + Chuang, + ChurchSlavic, + ChurchSlavonic, + Chuukese, + Chuvash, + ClassicalNepalBhasa, + ClassicalNewari, + ClassicalSyriac, + CookIslandsMaori, + Coptic, + Cornish, + Corsican, + Cree, + Creek, + CreolesAndPidgins, + CreolesAndPidginsEnglishBased, + CreolesAndPidginsFrenchBased, + CreolesAndPidginsPortugueseBased, + CrimeanTatar, + CrimeanTurkish, + Croatian, + CushiticLanguages, + Czech, + Dakota, + Danish, + Dargwa, + Delaware, + DeneSuline, + Dhivehi, + Dimili, + Dimli, + Dinka, + Divehi, + Dogri, + Dogrib, + DravidianLanguages, + Duala, + Dutch, + DutchMiddle, + Dyula, + Dzongkha, + EasternFrisian, + Edo, + Efik, + Egyptian, + Ekajuk, + Elamite, + English, + EnglishMiddle, + EnglishOld, + Erzya, + Esperanto, + Estonian, + Ewe, + Ewondo, + Fang, + Fanti, + Faroese, + Fijian, + Filipino, + Finnish, + FinnoUgrianLanguages, + Flemish, + Fon, + French, + FrenchMiddle, + FrenchOld, + Friulian, + Fulah, + Ga, + Gaelic, + GalibiCarib, + Galician, + Ganda, + Gayo, + Gbaya, + Geez, + Georgian, + German, + GermanLow, + GermanMiddleHigh, + GermanOldHigh, + GermanicLanguages, + Gikuyu, + Gilbertese, + Gondi, + Gorontalo, + Gothic, + Grebo, + GreekAncient, + GreekModern, + Greenlandic, + Guarani, + Gujarati, + Gwichin, + Haida, + Haitian, + HaitianCreole, + Hausa, + Hawaiian, + Hebrew, + Herero, + Hiligaynon, + HimachaliLanguages, + Hindi, + HiriMotu, + Hittite, + Hmong, + Hungarian, + Hupa, + Iban, + Icelandic, + Ido, + Igbo, + IjoLanguages, + Iloko, + ImperialAramaic, + InariSami, + IndicLanguages, + IndoEuropeanLanguages, + Indonesian, + Ingush, + Interlingua, + Interlingue, + Inuktitut, + Inupiaq, + IranianLanguages, + Irish, + IrishMiddle, + IrishOld, + IroquoianLanguages, + Italian, + Japanese, + Javanese, + Jingpho, + JudeoArabic, + JudeoPersian, + Kabardian, + Kabyle, + Kachin, + Kalaallisut, + Kalmyk, + Kamba, + Kannada, + Kanuri, + Kapampangan, + KaraKalpak, + KarachayBalkar, + Karelian, + KarenLanguages, + Kashmiri, + Kashubian, + Kawi, + Kazakh, + Khasi, + KhoisanLanguages, + Khotanese, + Kikuyu, + Kimbundu, + Kinyarwanda, + Kirdki, + Kirghiz, + Kirmanjki, + Klingon, + Komi, + Kongo, + Konkani, + Korean, + Kosraean, + Kpelle, + KruLanguages, + Kuanyama, + Kumyk, + Kurdish, + Kurukh, + Kutenai, + Kwanyama, + Kyrgyz, + Ladino, + Lahnda, + Lamba, + LandDayakLanguages, + Lao, + Latin, + Latvian, + Leonese, + Letzeburgesch, + Lezghian, + Limburgan, + Limburger, + Limburgish, + Lingala, + Lithuanian, + Lojban, + LowGerman, + LowSaxon, + LowerSorbian, + Lozi, + LubaKatanga, + LubaLulua, + Luiseno, + LuleSami, + Lunda, + Luo, + Lushai, + Luxembourgish, + MacedoRomanian, + Macedonian, + Madurese, + Magahi, + Maithili, + Makasar, + Malagasy, + Malay, + Malayalam, + Maldivian, + Maltese, + Manchu, + Mandar, + Mandingo, + Manipuri, + ManoboLanguages, + Manx, + Maori, + Mapuche, + Mapudungun, + Marathi, + Mari, + Marshallese, + Marwari, + Masai, + MayanLanguages, + Mende, + Mikmaq, + Micmac, + Minangkabau, + Mirandese, + Mohawk, + Moksha, + Moldavian, + Moldovan, + MonKhmerLanguages, + Mong, + Mongo, + Mongolian, + Mossi, + MultipleLanguages, + MundaLanguages, + NKo, + NahuatlLanguages, + Nauru, + Navaho, + Navajo, + NdebeleNorth, + NdebeleSouth, + Ndonga, + Neapolitan, + NepalBhasa, + Nepali, + Newari, + Nias, + NigerKordofanianLanguages, + NiloSaharanLanguages, + Niuean, + Nolinguisticcontent, + Nogai, + NorseOld, + NorthAmericanIndianLanguages, + NorthNdebele, + NorthernFrisian, + NorthernSami, + NorthernSotho, + Norwegian, + NorwegianBokmål, + NorwegianNynorsk, + Notapplicable, + NubianLanguages, + Nuosu, + Nyamwezi, + Nyanja, + Nyankole, + NynorskNorwegian, + Nyoro, + Nzima, + Occidental, + Occitan, + OccitanOld, + OfficialAramaic, + Oirat, + Ojibwa, + OldBulgarian, + OldChurchSlavonic, + OldNewari, + OldSlavonic, + Oriya, + Oromo, + Osage, + Ossetian, + Ossetic, + OtomianLanguages, + Pahlavi, + Palauan, + Pali, + Pampanga, + Pangasinan, + Panjabi, + Papiamento, + PapuanLanguages, + Pashto, + Pedi, + Persian, + PersianOld, + PhilippineLanguages, + Phoenician, + Pilipino, + Pohnpeian, + Polish, + Portuguese, + PrakritLanguages, + ProvençalOld, + Punjabi, + Pushto, + Quechua, + Rajasthani, + Rapanui, + Rarotongan, + ReservedForLocalUse, + RomanceLanguages, + Romanian, + Romansh, + Romany, + Rundi, + Russian, + Sakan, + SalishanLanguages, + SamaritanAramaic, + SamiLanguages, + Samoan, + Sandawe, + Sango, + Sanskrit, + Santali, + Sardinian, + Sasak, + SaxonLow, + Scots, + ScottishGaelic, + Selkup, + SemiticLanguages, + Sepedi, + Serbian, + Serer, + Shan, + Shona, + SichuanYi, + Sicilian, + Sidamo, + SignLanguages, + Siksika, + Sindhi, + Sinhala, + Sinhalese, + SinoTibetanLanguages, + SiouanLanguages, + SkoltSami, + Slave, + SlavicLanguages, + Slovak, + Slovenian, + Sogdian, + Somali, + SonghaiLanguages, + Soninke, + SorbianLanguages, + SothoNorthern, + SothoSouthern, + SouthAmericanIndianLanguages, + SouthNdebele, + SouthernAltai, + SouthernSami, + Spanish, + SrananTongo, + Sukuma, + Sumerian, + Sundanese, + Susu, + Swahili, + Swati, + Swedish, + SwissGerman, + Syriac, + Tagalog, + Tahitian, + TaiLanguages, + Tajik, + Tamashek, + Tamil, + Tatar, + Telugu, + Tereno, + Tetum, + Thai, + Tibetan, + Tigre, + Tigrinya, + Timne, + Tiv, + tlhInganHol, + Tlingit, + TokPisin, + Tokelau, + TongaNyasa, + TongaTongaIslands, + Tsimshian, + Tsonga, + Tswana, + Tumbuka, + TupiLanguages, + Turkish, + TurkishOttoman, + Turkmen, + Tuvalu, + Tuvinian, + Twi, + Udmurt, + Ugaritic, + Uighur, + Ukrainian, + Umbundu, + UncodedLanguages, + Undetermined, + UpperSorbian, + Urdu, + Uyghur, + Uzbek, + Vai, + Valencian, + Venda, + Vietnamese, + Volapük, + Votic, + WakashanLanguages, + Walloon, + Waray, + Washo, + Welsh, + WesternFrisian, + WesternPahariLanguages, + Wolaitta, + Wolaytta, + Wolof, + Xhosa, + Yakut, + Yao, + Yapese, + Yiddish, + Yoruba, + YupikLanguages, + ZandeLanguages, + Zapotec, + Zaza, + Zazaki, + Zenaga, + Zhuang, + Zulu, + Zuni + } + + public class TrackEntry + { + // `private const string' would increase the .exe size by 5 kB here. + private static readonly string LanguageCodes = "abkaceachadaadyadyaarafhafrafaainakaakkalbgswalealggswtutamhanpapaaraargarparwarmrupartrupasmastastathausmapavaaveawaaymazeastbanbatbalbambaibadbntbasbakbaqbtkbejbejbelbembenberbhobihbikbynbinbisbynzblzblzblnobbosbrabrebugbulbuaburcadspacatcaucebcelcaikhmchgcmcchachechrnyachychbnyachichnchpchozhachuchuchkchvnwcnwcsycrarcopcorcoscremuscrpcpecpfcppcrhcrhhrvcusczedakdandardelchpdivzzazzadindivdoidgrdraduadutdumdyudzofrsbinefiegyekaelxengenmangmyvepoesteweewofanfatfaofijfilfinfiudutfonfrefrmfrofurfulgaaglacarglgluggaygbagezgeogerndsgmhgohgemkikgilgongorgotgrbgrcgrekalgrngujgwihaihathathauhawhebherhilhimhinhmohithmnhunhupibaiceidoiboijoiloarcsmnincineindinhinaileikuipkiraglemgasgairoitajpnjavkacjrbjprkbdkabkackalxalkamkankaupamkaakrckrlkarkascsbkawkazkhakhikhokikkmbkinzzakirzzatlhkomkonkokkorkoskpekrokuakumkurkrukutkuakirladlahlamdaylaolatlavastltzlezlimlimlimlinlitjbondsndsdsblozlublualuismjlunluolusltzrupmacmadmagmaimakmlgmaymaldivmltmncmdrmanmnimnoglvmaoarnarnmarchmmahmwrmasmynmenmicmicminmwlmohmdfrumrummkhhmnlolmonmosmulmunnqonahnaunavnavndenblndonapnewnepnewnianicssaniuzxxnognonnaindefrrsmensonornobnnozxxnubiiinymnyanynnnonyonziileociproarcxalojichuchunwcchuoriormosaossossotopalpauplipampagpanpappaapusnsoperpeophiphnfilponpolporprapropanpusquerajraprarqaaroarumrohromrunruskhosalsamsmismosadsagsansatsrdsasndsscoglaselsemnsosrpsrrshnsnaiiiscnsidsgnblasndsinsinsitsiosmsdenslasloslvsogsomsonsnkwennsosotsainblaltsmaspasrnsuksuxsunsusswasswswegswsyrtgltahtaitgktmhtamtatteltertetthatibtigtirtemtivtlhtlitpitkltogtontsitsotsntumtupturotatuktvltyvtwiudmugauigukrumbmisundhsburduiguzbvaicatvenvievolvotwakwlnwarwaswelfryhimwalwalwolxhosahyaoyapyidyorypkzndzapzzazzazenzhazulzun"; + private static string GetLanguageCode(LanguageID id) + { + int i = (int)id; + int i3 = i * 3; + if (i < 0 || i3 >= LanguageCodes.Length) + { + throw new Exception(string.Format("LanguageID '{0}' is unsupported!", id)); + } + return LanguageCodes.Substring(i3, 3); + } + + // TODO: Change public to private. + public ulong TrackNumber; + public TrackType TrackType; + public string Name; + public LanguageID Language = LanguageID.English; + public CodecID CodecID; + private byte[] CodecPrivate; + private byte[] InfoBytes; + public TrackEntry(TrackType trackType, byte[] infoBytes, CodecID codecID, byte[] codecPrivate) + { + this.TrackType = trackType; + this.InfoBytes = infoBytes; + this.CodecID = codecID; + this.CodecPrivate = codecPrivate; + } + public byte[] GetBytes() + { + if (this.TrackNumber == 0uL) + { + throw new Exception("TrackNumber must be greater than 0!"); + } + ulong trackUID = this.TrackNumber; + if (trackUID == 0uL) + { + throw new Exception("TrackUID must be greater than 0!"); + } + List list = new List(); + list.Add(MkvUtils.GetEEBytes(ID.TrackNumber, MkvUtils.GetVintBytes(this.TrackNumber))); + list.Add(MkvUtils.GetEEBytes(ID.TrackUID, MkvUtils.GetVintBytes(trackUID))); + list.Add(MkvUtils.GetEEBytes(ID.TrackType, MkvUtils.GetVintBytes((ulong)this.TrackType))); + list.Add(MkvUtils.GetEEBytes(ID.FlagEnabled, MkvUtils.GetVIntForFlag(true))); + list.Add(MkvUtils.GetEEBytes(ID.FlagDefault, MkvUtils.GetVIntForFlag(true))); + list.Add(MkvUtils.GetEEBytes(ID.FlagForced, MkvUtils.GetVIntForFlag(false))); + list.Add(MkvUtils.GetEEBytes(ID.FlagLacing, MkvUtils.GetVIntForFlag(true))); + if (!string.IsNullOrEmpty(this.Name)) + { + list.Add(MkvUtils.GetEEBytes(ID.Name, Encoding.UTF8.GetBytes(this.Name))); + } + if (this.Language != LanguageID.English) + { + list.Add(MkvUtils.GetEEBytes(ID.Language, Encoding.ASCII.GetBytes(GetLanguageCode(this.Language)))); + } + list.Add(MkvUtils.GetEEBytes(ID.CodecID, Encoding.ASCII.GetBytes(MkvUtils.GetStringForCodecID(this.CodecID)))); + if (this.CodecPrivate != null) + { + list.Add(MkvUtils.GetEEBytes(ID.CodecPrivate, this.CodecPrivate)); + } + list.Add(this.InfoBytes); + return MkvUtils.GetEEBytes(ID.TrackEntry, Utils.CombineByteArrays(list)); + } + } + + // This is a pure data class, please don't add logic. + public class MediaDataBlock + { + // The media sample data bytes (i.e. a frame from the media file). + public ArraySegment Bytes; + public ulong StartTime; + public bool IsKeyFrame; + + public MediaDataBlock(ArraySegment bytes, ulong startTime, bool isKeyFrame) + { + this.Bytes = bytes; + this.StartTime = startTime; + this.IsKeyFrame = isKeyFrame; + } + } + + public interface IChunkStartTimeReceiver + { + // Returns ulong.MaxValue if the information is not available (i.e. chunkIndex is too large). + ulong GetChunkStartTime(int trackIndex, int chunkIndex); + void SetChunkStartTime(int trackIndex, int chunkIndex, ulong chunkStartTime); + } + + // A source (forward-iterator) of MediaDataBlock objects in a fixed number of parallel tracks. + public interface IMediaDataSource + { + int GetTrackCount(); + // Called only when all chunks have been read (using ConsumeBlock). + ulong GetTrackEndTime(int trackIndex); + void StartChunks(IChunkStartTimeReceiver chunkStartTimeReceiver); + // Returns the first MediaDataBlock (i.e. with smallest StartTime) unconsumed MediaDataBlock on the specified track, or + // null on EOF on the specified track. The ownership of the returned + // block is shared between the MediaDataSource and the caller until ConsumeBlock(trackIndex) is called. Afterwards the + // the MediaDataSource releases ownership. The MediaDataSource never modifies the fields of the MediaDataBlock or the + // contents of its .Bytes array it returns, and + // it returns the same reference until ConsumeSample(trackIndex) is called, and a different reference afterwards. + MediaDataBlock PeekBlock(int trackIndex); + // Consumes the first unconsumed data block on the specified track. It's illegal to call this method if there are no + // unconsumed MediaDataBlock objects left on the specified track. + void ConsumeBlock(int trackIndex); + // Consume all blocks with .StartTime <= startTime from the specified track. + void ConsumeBlocksUntil(int trackIndex, ulong startTime); + } + + public class MuxStateWriter + { + private Stream Stream; + public MuxStateWriter(Stream stream) + { + this.Stream = stream; + } + public void Close() + { + this.Stream.Close(); + } + public void Flush() + { + this.Stream.Flush(); + } + // TODO: Use a binary format to save space (maybe 50% of the mux state file size). + public void WriteUlong(char key, ulong num) + { + // TODO: Speed this up if necessary. + byte[] outputBytes = Encoding.ASCII.GetBytes(("" + key) + num + '\n'); + this.Stream.Write(outputBytes, 0, outputBytes.Length); + } + public void WriteBytes(char key, byte[] bytes) + { + // TODO: Speed this up if necessary. + byte[] outputBytes = Encoding.ASCII.GetBytes(key + ":" + Utils.HexEncodeString(bytes) + '\n'); + this.Stream.Write(outputBytes, 0, outputBytes.Length); + } + public void WriteRaw(byte[] bytes, int start, int end) + { + this.Stream.Write(bytes, start, end - start); + } + } + + public class MkvUtils + { + private const ulong DATA_SIZE_MAX_VALUE = 72057594037927934uL; + public static byte[] GetDataSizeBytes(ulong value) + { + if (value > DATA_SIZE_MAX_VALUE) + { + throw new Exception(string.Format("Data size '{0}' is greater than its max value!", value)); + } + byte[] bytes = BitConverter.GetBytes(value); + Array.Reverse(bytes); + int b = 1; + while (value > (1uL << (7 * b)) - 2) + { + ++b; + } + byte[] array = new byte[b]; + Buffer.BlockCopy(bytes, 8 - b, array, 0, b); + array[0] += (byte)(1 << (8 - b)); + return array; + } + // Like GetDataSizeBytes, but always returns the longest possible byte array (of size 8). + private static byte[] GetDataSizeEightBytes(ulong value) + { + if (value > DATA_SIZE_MAX_VALUE) + { + throw new Exception(string.Format("Data size '{0}' is greater than its max value!", value)); + } + byte[] bytes = BitConverter.GetBytes(value); + Array.Reverse(bytes); + bytes[0] = 1; + return bytes; + } + private const ulong VINT_MAX_VALUE = 18446744073709551615uL; + public static byte[] GetVintBytes(ulong value) + { + byte[] bytes = BitConverter.GetBytes(value); + Array.Reverse(bytes); + int b = 0; + while (bytes[b] == 0 && b + 1 < bytes.Length) + { + ++b; + } + byte[] array = new byte[bytes.Length - b]; + for (int i = 0; i < array.Length; i++) + { + array[i] = bytes[b + i]; + } + return array; + } + public static byte[] GetFloatBytes(float value) + { + return Utils.InplaceReverseBytes(BitConverter.GetBytes(value)); + } + private static readonly DateTime MinDateTimeValue = DateTime.Parse("2001-01-01").ToUniversalTime(); + public static byte[] GetDateTimeBytes(DateTime dateTime) + { + DateTime dateTime2 = dateTime.ToUniversalTime(); + if (dateTime2 < MinDateTimeValue) + { + throw new Exception(string.Format("Date '{0}' is lower than its min value!", dateTime.ToShortDateString())); + } + return Utils.InplaceReverseBytes(BitConverter.GetBytes(Convert.ToUInt64( + dateTime2.Subtract(MinDateTimeValue).TotalMilliseconds * 1000000.0))); + } + // Get EBML element bytes. + public static byte[] GetEEBytes(ID id, byte[] contents) + { + return Utils.CombineBytes(GetDataSizeBytes((ulong)id), + GetDataSizeBytes((ulong)contents.Length), + contents); + } + private static byte[] GetEbmlHeaderBytes() + { + List list = new List(); + list.Add(GetEEBytes(ID.EBMLVersion, GetVintBytes(1uL))); + list.Add(GetEEBytes(ID.EBMLReadVersion, GetVintBytes(1uL))); + list.Add(GetEEBytes(ID.EBMLMaxIDLength, GetVintBytes(4uL))); + list.Add(GetEEBytes(ID.EBMLMaxSizeLength, GetVintBytes(8uL))); + list.Add(GetEEBytes(ID.DocType, Encoding.ASCII.GetBytes("matroska"))); + list.Add(GetEEBytes(ID.DocTypeVersion, GetVintBytes(1uL))); + list.Add(GetEEBytes(ID.DocTypeReadVersion, GetVintBytes(1uL))); + return GetEEBytes(ID.EBML, Utils.CombineByteArrays(list)); + } + public static string GetStringForCodecID(CodecID codecID) + { + switch (codecID) + { + case CodecID.V_AVC: { return "V_MPEG4/ISO/AVC"; } + case CodecID.V_MS: { return "V_MS/VFW/FOURCC"; } + case CodecID.A_AAC: { return "A_AAC"; } + case CodecID.A_MS: { return "A_MS/ACM"; } + default: { throw new Exception(string.Format("CodecID '{0}' is invalid!", codecID)); } + } + } + public static byte[] GetVideoInfoBytes(ulong pixelWidth, ulong pixelHeight, ulong displayWidth, ulong displayHeight) + { + if (pixelWidth == 0uL) + { + throw new Exception("PixelWidth must be greater than 0!"); + } + if (pixelHeight == 0uL) + { + throw new Exception("PixelHeight must be greater than 0!"); + } + if (displayWidth == 0uL) + { + throw new Exception("DisplayWidth must be greater than 0!"); + } + if (displayHeight == 0uL) + { + throw new Exception("DisplayHeight must be greater than 0!"); + } + List list = new List(); + list.Add(GetEEBytes(ID.FlagInterlaced, GetVIntForFlag(false))); + list.Add(GetEEBytes(ID.PixelWidth, GetVintBytes(pixelWidth))); + list.Add(GetEEBytes(ID.PixelHeight, GetVintBytes(pixelHeight))); + if (displayWidth != pixelWidth) + { + list.Add(GetEEBytes(ID.DisplayWidth, GetVintBytes(displayWidth))); + } + if (displayHeight != pixelHeight) + { + list.Add(GetEEBytes(ID.DisplayHeight, GetVintBytes(displayHeight))); + } + return GetEEBytes(ID.Video, Utils.CombineByteArrays(list)); + } + public static byte[] GetAudioInfoBytes(float samplingFrequency, ulong channels, ulong bitDepth) + { + if (samplingFrequency <= 0f) + { + throw new Exception("SamplingFrequency must be greater than 0!"); + } + if (channels == 0uL) + { + throw new Exception("Channels cannot be 0!"); + } + List list = new List(); + list.Add(GetEEBytes(ID.SamplingFrequency, GetFloatBytes(samplingFrequency))); + list.Add(GetEEBytes(ID.Channels, GetVintBytes(channels))); + if (bitDepth != 0uL) + { + list.Add(GetEEBytes(ID.BitDepth, GetVintBytes(bitDepth))); + } + return GetEEBytes(ID.Audio, Utils.CombineByteArrays(list)); + } + private static readonly byte[] VINT_FALSE = new byte[] { 0 }; + private static readonly byte[] VINT_TRUE = new byte[] { 1 }; + public static byte[] GetVIntForFlag(bool flag) + { + return flag ? VINT_TRUE : VINT_FALSE; + } + private static byte[] GetDurationBytes(ulong duration, ulong timeScale) + { + float floatDuration = Convert.ToSingle(duration * 1000.0 / timeScale); + if (floatDuration <= 0f) floatDuration = 0.125f; // .mkv requires a positive duration. + byte[] bytes = BitConverter.GetBytes(floatDuration); // 4 bytes. + Array.Reverse(bytes); + return bytes; + } + + private static byte[] GetSegmentInfoBytes(ulong duration, ulong timeScale, bool isDeterministic) + { + AssemblyName name = Assembly.GetEntryAssembly().GetName(); + string muxingApp = name.Name + " v" + name.Version; + string writingApp = muxingApp; + byte[] segmentUid; + if (isDeterministic) + { + // 16 bytes; seemingly random, but deterministic. + segmentUid = new byte[] { 110, 104, 17, 204, 142, 130, 251, 240, 218, 112, 216, 160, 143, 114, 2, 237 }; + } + else + { + segmentUid = new byte[16]; + new Random().NextBytes(segmentUid); + } + List list = new List(); + // ID.Duration must be the first in the list so FindDurationOffset can find it. + list.Add(GetEEBytes(ID.Duration, GetDurationBytes(duration, timeScale))); + if (!string.IsNullOrEmpty(muxingApp)) + { + list.Add(GetEEBytes(ID.MuxingApp, Encoding.ASCII.GetBytes(muxingApp))); + } + if (!string.IsNullOrEmpty(writingApp)) + { + list.Add(GetEEBytes(ID.WritingApp, Encoding.ASCII.GetBytes(writingApp))); + } + list.Add(GetEEBytes(ID.SegmentUID, segmentUid)); + // The deterministic date was a few minutes before Tue Apr 17 21:14:22 CEST 2012. + byte[] dateBytes = isDeterministic ? new byte[] { 4, 242, 35, 97, 249, 143, 0, 192 } + : GetDateTimeBytes(DateTime.UtcNow); + list.Add(GetEEBytes(ID.DateUTC, dateBytes)); + list.Add(GetEEBytes(ID.TimecodeScale, GetVintBytes(timeScale / 10uL))); + return GetEEBytes(ID.Info, Utils.CombineByteArrays(list)); + } + + private static byte[] GetTrackEntriesBytes(IList trackEntries) + { + byte[][] byteArrays = new byte[trackEntries.Count][]; + for (int i = 0; i < trackEntries.Count; i++) + { + byteArrays[i] = trackEntries[i].GetBytes(); + } + return GetEEBytes(ID.Tracks, Utils.CombineByteArrays(byteArrays)); + } + + // This is a pure data struct. Please don't add functionality. + private struct SeekBlock + { + public ID ID; + public ulong Offset; + public SeekBlock(ID id, ulong offset) + { + this.ID = id; + this.Offset = offset; + } + } + + private static byte[] GetVoidBytes(ulong length) + { + if (length < 9uL) + { + // >=9 == 1 byte for ID.Void, 8 bytes for the fixed length and >=0 bytes for the data. + throw new Exception("Void must be greater than or equal to 9 bytes."); + } + length -= 9; + return Utils.CombineBytes(GetDataSizeBytes((ulong)ID.Void), + GetDataSizeEightBytes(length), + new byte[length]); + } + + private static byte[] GetSeekBytes(IList seekBlocks, int desiredSize) + { + int seekBlockCount = seekBlocks.Count; + byte[][] byteArrays = new byte[4 * seekBlockCount + 3][]; + byteArrays[0] = GetDataSizeBytes((ulong)ID.SeekHead); + for (int i = 0, j = 2; i < seekBlockCount; ++i, j += 4) + { + byteArrays[j] = GetDataSizeBytes((ulong)ID.Seek); + byteArrays[j + 2] = GetEEBytes(ID.SeekID, GetDataSizeBytes((ulong)seekBlocks[i].ID)); + byteArrays[j + 3] = GetEEBytes(ID.SeekPosition, GetVintBytes(seekBlocks[i].Offset)); + byteArrays[j + 1] = GetDataSizeBytes((ulong)(byteArrays[j + 2].Length + byteArrays[j + 3].Length)); + } + int dataSize = 0; + int voidIndex = byteArrays.Length - 1; + for (int i = 2; i < voidIndex; ++i) + { + dataSize += byteArrays[i].Length; + } + byteArrays[1] = GetDataSizeBytes((ulong)dataSize); + byteArrays[voidIndex] = new byte[] { }; + if (desiredSize >= 0) + { + dataSize += byteArrays[0].Length + byteArrays[1].Length; + if (desiredSize != dataSize) + { + if (desiredSize <= dataSize + 9) + { + throw new Exception("dataSize too small, got " + dataSize + ", expected <=" + (desiredSize - 9)); + } + byteArrays[voidIndex] = GetVoidBytes((ulong)(desiredSize - dataSize)); + } + } + return Utils.CombineByteArrays(byteArrays); + } + + private const int DESIRED_SEEK_SIZE = 90; + + private static byte[] GetSegmentBytes(ulong duration, ulong mediaEndOffsetMS, + ulong seekHeadOffsetMS, ulong cuesOffsetMS, + ulong timeScale, IList trackEntries, bool isDeterministic) + { + byte[][] byteArrays = new byte[5][]; + byteArrays[0] = GetDataSizeBytes((ulong)ID.Segment); // 4 bytes. + // Segment data size. + byteArrays[1] = GetDataSizeEightBytes(mediaEndOffsetMS); // 1 byte header (== 1) + 7 bytes of size. + // byteArrays[2][0] is at segmentOffset. + // byteArrays[2] will be an ID.SeekHead + ID.Void at a total size of DESIRED_SEEK_SIZE. + byteArrays[3] = GetSegmentInfoBytes(duration, timeScale, isDeterministic); + byteArrays[4] = GetTrackEntriesBytes(trackEntries); + + IList seekBlocks = new List(); + seekBlocks.Add(new SeekBlock(ID.Info, DESIRED_SEEK_SIZE)); + seekBlocks.Add(new SeekBlock(ID.Tracks, (ulong)(DESIRED_SEEK_SIZE + byteArrays[3].Length))); + if (seekHeadOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.SeekHead, seekHeadOffsetMS)); + if (cuesOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.Cues, cuesOffsetMS)); + byteArrays[2] = GetSeekBytes(seekBlocks, DESIRED_SEEK_SIZE); + + return Utils.CombineByteArrays(byteArrays); + // * The first 4 bytes of the return value are from GetDataSizeBytes((ulong)ID.Segment). + // * The next 1 byte of the return value is 1, the prefix of the 7-byte data size in datasize.GetUInt64(). + // * The next 7 bytes of the return value the total size of `list', but that doesn't matter, because it would be + // overwritten just after WriteMkv has written all media data and cues to the file (so the total file size is known). + // return GetEEBytes(ID.Segment, GetEBMLBytes(list), true); + } + + // Can't be larger, because the datasize class cannot serialize much larger values than that. + private const ulong INITIAL_MEDIA_END_OFFSET_MS = ulong.MaxValue >> 9; + private const ulong INITIAL_SEEK_HEAD_OFFSET_MS = 0; + private const ulong INITIAL_CUES_OFFSET_MS = 0; + private const ulong KEEP_ORIGINAL_DURATION = ulong.MaxValue - 1; + + // Returns the first offset not updated. + // timeScale is ignored if duration == KEEP_ORIGINAL_DURATION. + private static int UpdatePrefix(byte[] prefix, int prefixSize, + ulong segmentOffset, ulong mediaEndOffsetMS, ulong seekHeadOffsetMS, ulong cuesOffsetMS, + ulong duration, ulong timeScale) + { + Buffer.BlockCopy(Utils.InplaceReverseBytes(BitConverter.GetBytes(mediaEndOffsetMS)), 1, + prefix, (int)segmentOffset - 7, 7); + int durationOffset; + int afterInfoOffset; + FindDurationAndAfterInfoOffset(prefix, (int)segmentOffset, prefixSize, out durationOffset, out afterInfoOffset); + if (duration != KEEP_ORIGINAL_DURATION) + { + Buffer.BlockCopy(GetDurationBytes(duration, timeScale), 0, prefix, durationOffset, 4); + } + IList seekBlocks = new List(); + seekBlocks.Add(new SeekBlock(ID.Info, DESIRED_SEEK_SIZE)); + seekBlocks.Add(new SeekBlock(ID.Tracks, (ulong)afterInfoOffset - segmentOffset)); + if (seekHeadOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.SeekHead, seekHeadOffsetMS)); + if (cuesOffsetMS > 0) seekBlocks.Add(new SeekBlock(ID.Cues, cuesOffsetMS)); + byte[] seekBytes = GetSeekBytes(seekBlocks, DESIRED_SEEK_SIZE); + Buffer.BlockCopy(seekBytes, 0, prefix, (int)segmentOffset, seekBytes.Length); + return durationOffset + 4; + } + + private static int GetEbmlElementDataSize(byte[] bytes, ref int i) + { + // Width Size Representation + // 1 2^7 1xxx xxxx + // 2 2^14 01xx xxxx xxxx xxxx + // 3 2^21 001x xxxx xxxx xxxx xxxx xxxx + // 4 2^28 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx + // ..7. + if (bytes.Length <= i) + { + throw new Exception("EOF in EBML length."); + } + if ((bytes[i] & 0x80) != 0) + { + return bytes[i++] & 0x7f; + } + else if ((bytes[i] & 0x40) != 0) + { + i += 2; + if (bytes.Length < i) + { + throw new Exception("EOF in EBML length 2."); + } + return (bytes[i - 2] & 0x3f) << 8 | bytes[i - 1]; + } + else if (bytes[i] == 1) + { + i += 8; + if (bytes.Length < i) + { + throw new Exception("EOF in EBML length 8."); + } + if (bytes[i - 5] != 0 || bytes[i - 6] != 0 || bytes[i - 7] != 0 || (bytes[i - 4] & 0x80) != 0) + { + throw new Exception("EBML length 8 too large for an int."); + } + return bytes[i - 1] | bytes[i - 2] << 8 | bytes[i - 3] << 16 | bytes[i - 4] << 24; + } + else + { + throw new Exception("Long EBML elements not implemented."); + } + } + + // Sets durationOffset to the 4 bytes in `bytes' containing the floatDuration field. + // Sets afterInfoOffset to the offset right after the ID.Info element. + // `bytes' is the prefix on an .mkv file written by us, with ID.Segment starting at segmentOffset or 0. + // `j' is the end offset in bytes. + private static void FindDurationAndAfterInfoOffset(byte[] bytes, int segmentOffset, int j, + out int durationOffset, out int afterInfoOffset) + { + int i = segmentOffset; + // Skip ID.EBML if present. + // if (i + 4 <= j && bytes[i] == 26 && bytes[i + 1] == 69 && bytes[i + 2] == 223 && bytes[i + 3] == 163) { + // i += 4; i += ... GetEbmlElementDataSize(bytes, ref i); + // } + // Skip ID.SeekHead if present. + if (i + 4 <= j && bytes[i] == 17 && bytes[i + 1] == 77 && bytes[i + 2] == 155 && bytes[i + 3] == 116) + { + i += 4; + int n = GetEbmlElementDataSize(bytes, ref i); // Doesn't work (i becomes 68 instead of 76) without a helper. + i += n; + } + // Skip ID.Void if present. + if (i < j && bytes[i] == 236) + { + ++i; + int n = GetEbmlElementDataSize(bytes, ref i); // Doesn't work (i becomes 68 instead of 76) without a helper. + i += n; + } + // Detect ID.Info. + if (!(i + 4 <= j && bytes[i] == 21 && bytes[i + 1] == 73 && bytes[i + 2] == 169 && bytes[i + 3] == 102)) + { + throw new Exception("Expected ID.Info."); + } + i += 4; + int infoSize = GetEbmlElementDataSize(bytes, ref i); + afterInfoOffset = i + infoSize; + if (j > i + infoSize) j = i + infoSize; + // Detect ID.Duration. + if (!(i + 2 <= j && bytes[i] == 68 && bytes[i + 1] == 137)) + { + throw new Exception("Expected ID.Duration."); + } + i += 2; + int durationSize = GetEbmlElementDataSize(bytes, ref i); + if (durationSize != 4) + { + throw new Exception("Bad durationSize."); + } + durationOffset = i; + } + + private static int GetVideoTrackIndex(IList trackEntries, int defaultIndex) + { + int videoTrackIndex = 0; + while (videoTrackIndex < trackEntries.Count && trackEntries[videoTrackIndex].TrackType != TrackType.Video) + { + ++videoTrackIndex; + } + return (videoTrackIndex == trackEntries.Count) ? defaultIndex : videoTrackIndex; + } + + private static IList GetIsAmsCodecs(IList trackEntries) + { + IList isAmsCodecs = new List(); + for (int i = 0; i < trackEntries.Count; ++i) + { + isAmsCodecs.Add(trackEntries[i].CodecID == CodecID.A_MS); + } + return isAmsCodecs; + } + + private static byte[] GetSimpleBlockBytes(ulong trackNumber, short timeCode, bool IsKeyFrame, bool isAmsCodec, + int mediaDataBlockTotalSize) + { + // Was: LacingID lacingId = isAmsCodec ? LacingID.FixedSize : LacingID.No; + byte b = isAmsCodec ? (byte)4 : (byte)0; + if (IsKeyFrame) + { + b += 128; + } + // Originally b was always initialized to 0, and then incremented like this: + // switch (lacingId) { + // case LacingID.No: { break; } + // case LacingID.Xiph: { b += 2; break; } + // case LacingID.EBML: { b += 6; break; } + // case LacingID.FixedSize: { b += 4; break; } + // } + List output = new List(); + output.Add(GetDataSizeBytes((ulong)ID.SimpleBlock)); + output.Add(null); // Reserved for the return value of GetDataSizeBytes. + output.Add(GetDataSizeBytes(trackNumber)); + output.Add(Utils.InplaceReverseBytes(BitConverter.GetBytes(timeCode))); + output.Add(new byte[] { b }); + // Was: if (lacingId != LacingID.No) output.Add(new byte[] { (byte)(sampleData.Count - 1) }); + if (isAmsCodec) output.Add(new byte[] { (byte)1 }); + int totalSize = 0; + for (int i = 2; i < output.Count; ++i) + { + totalSize += output[i].Length; + } + output[1] = GetDataSizeBytes((ulong)(totalSize + mediaDataBlockTotalSize)); + // Usually output[0].Length == 3, and the length of the rest of output (without sampleData) is 4. + return Utils.CombineByteArrays(output); + } + + public static byte[] GetCueBytes(IList cuePoints) + { + byte[][] output = new byte[cuePoints.Count][]; + for (int i = 0; i < cuePoints.Count; i++) + { + output[i] = cuePoints[i].GetBytes(); + } + // TODO: Avoid unnecessary copies, also in GetEEBytes. + return GetEEBytes(ID.Cues, Utils.CombineByteArrays(output)); + } + + private class StateChunkStartTimeReceiver : IChunkStartTimeReceiver + { + private MuxStateWriter MuxStateWriter; + private IList[] TrackChunkStartTimes; + private int[] TrackChunkWrittenCounts; + // Takes ownership of trackChunkStartTimes (and will append to its items). + public StateChunkStartTimeReceiver(MuxStateWriter muxStateWriter, IList[] trackChunkStartTimes) + { + this.MuxStateWriter = muxStateWriter; + this.TrackChunkStartTimes = trackChunkStartTimes; + this.TrackChunkWrittenCounts = new int[trackChunkStartTimes.Length]; // Initializes items to 0. + for (int trackIndex = 0; trackIndex < trackChunkStartTimes.Length; ++trackIndex) + { + IList chunkStartTimes = trackChunkStartTimes[trackIndex]; + if (chunkStartTimes == null) + { + trackChunkStartTimes[trackIndex] = chunkStartTimes = new List(); + } + else + { + int chunkCount = chunkStartTimes.Count; + for (int chunkIndex = 1; chunkIndex < chunkCount; ++chunkIndex) + { + if (chunkStartTimes[chunkIndex - 1] >= chunkStartTimes[chunkIndex]) + { + throw new Exception(string.Concat(new object[] { + "Chunk StartTimes not increasing: track=", trackIndex, + " chuunk=", chunkIndex })); + } + } + } + this.TrackChunkWrittenCounts[trackIndex] = trackChunkStartTimes[trackIndex].Count; + } + } + /*implements*/ + public ulong GetChunkStartTime(int trackIndex, int chunkIndex) + { + IList chunkStartTimes = this.TrackChunkStartTimes[trackIndex]; + return chunkIndex >= chunkStartTimes.Count ? ulong.MaxValue : chunkStartTimes[chunkIndex]; + } + /*implements*/ + public void SetChunkStartTime(int trackIndex, int chunkIndex, ulong chunkStartTime) + { + int chunkCount = this.TrackChunkStartTimes[trackIndex].Count; + if (chunkIndex == chunkCount) + { // A simple append. + IList chunkStartTimes = this.TrackChunkStartTimes[trackIndex]; + if (chunkCount > 0) + { + ulong lastChunkStartTime = chunkStartTimes[chunkCount - 1]; + if (lastChunkStartTime >= chunkStartTime) + { + throw new Exception(string.Concat(new object[] { + "New chunk StartTime not larger: track=", trackIndex, " chunk=", chunkIndex, + " last=", lastChunkStartTime, " new=", chunkStartTime })); + } + } + chunkStartTimes.Add(chunkStartTime); + ++chunkCount; + // Flush all chunk StartTimes not written to the .muxstate yet. Usually we write only one item + // (chunkStartTime) here. + int i = this.TrackChunkWrittenCounts[trackIndex]; + char key = (char)('n' + trackIndex); + if (i == 0) this.MuxStateWriter.WriteUlong(key, chunkStartTimes[i++]); + for (; i < chunkCount; ++i) + { + this.MuxStateWriter.WriteUlong(key, chunkStartTimes[i] - chunkStartTimes[i - 1]); + } + // There is no need to call this.MuxStateWriter.Flush(); here. it's OK to flush that later. + this.TrackChunkWrittenCounts[trackIndex] = i; + } + else if (chunkIndex < chunkCount) + { + ulong oldChunkStartTime = this.TrackChunkStartTimes[trackIndex][chunkIndex]; + if (chunkStartTime != oldChunkStartTime) + { + throw new Exception(string.Concat(new object[] { + "Chunk StartTime mismatch: track=", trackIndex, " chunk=", chunkIndex, + " old=", oldChunkStartTime, " new=", chunkStartTime })); + } + } + else + { + throw new Exception(string.Concat(new object[] { + "Chunk StartTime set too far: track=", trackIndex, " chunk=", chunkIndex, + " chunkCount=" + chunkCount })); + } + } + } + + // Calls fileStream.Position and fileStream.Write only. + // + // Usually trackSamples has 2 elements: a video track and an audio track. + // + // Uses trackEntries and trackSamples only as a read-only argument, doesn't modify their contents. + // + // Starts with the initial cue points specified in cuePoints, and appends subsequent cue points in place. + private static void WriteClustersAndCues(FileStream fileStream, + ulong segmentOffset, + int videoTrackIndex, + IList isAmsCodecs, + IMediaDataSource mediaDataSource, + MuxStateWriter muxStateWriter, + IList cuePoints, + ref ulong minStartTime, + ulong timePosition, + out ulong seekHeadOffsetMS, + out ulong cuesOffsetMS) + { + int trackCount = mediaDataSource.GetTrackCount(); + if (isAmsCodecs.Count != trackCount) + { + throw new Exception("ASSERT: isAmsCodecs vs mediaDataSource length mismatch."); + } + if (trackCount > 13) + { // 13 is because a..m and n..z in MuxStateWriter checkpointing. + throw new Exception("Too many tracks to mux."); + } + // For each track, contains the data bytes of a media sample ungot (i.e. pushed back) after reading. + // Initializes items to null (good). + MediaDataBlock[] ungetBlocks = new MediaDataBlock[trackCount]; + ulong minStartTime0 = minStartTime; + if (timePosition == ulong.MaxValue) + { + timePosition = 0; + ulong maxStartTime = ulong.MaxValue; + for (int i = 0; i < trackCount; ++i) + { + if ((ungetBlocks[i] = mediaDataSource.PeekBlock(i)) != null) + { + if (maxStartTime == ulong.MaxValue || maxStartTime < ungetBlocks[i].StartTime) + { + maxStartTime = ungetBlocks[i].StartTime; + } + mediaDataSource.ConsumeBlock(i); // Since it was moved to ungetBlocks[i]. + } + } + for (int i = 0; i < trackCount; ++i) + { + MediaDataBlock block = mediaDataSource.PeekBlock(i); + while (block != null && block.StartTime <= maxStartTime) + { + ungetBlocks[i] = block; // Takes ownership. + mediaDataSource.ConsumeBlock(i); + } + // We'll start each track (in ungetMediaSample[i]) from the furthest sample within maxStartTime. + } + int trackIndex2; + if ((trackIndex2 = GetNextTrackIndex(mediaDataSource, ungetBlocks)) < 0) + { + throw new Exception("ASSERT: Empty media file, no samples."); + } + minStartTime = minStartTime0 = ungetBlocks[trackIndex2] != null ? ungetBlocks[trackIndex2].StartTime : + mediaDataSource.PeekBlock(trackIndex2).StartTime; + muxStateWriter.WriteUlong('A', minStartTime0); + } + List> output = new List>(); + ulong[] lastOutputStartTimes = new ulong[trackCount]; // Items initialized to zero. + int trackIndex; + // timePosition is the beginning StartTime of the last output block written by fileStream.Write. + while ((trackIndex = GetNextTrackIndex(mediaDataSource, ungetBlocks)) >= 0) + { + ulong timeCode; // Will be set below. + bool isKeyFrame; // Will be set below. + MediaDataBlock block0; // Will be set below. + MediaDataBlock block1 = null; // May be set below. + int mediaDataBlockTotalSize; // Will be set below. + { + if ((block0 = ungetBlocks[trackIndex]) == null && + (block0 = mediaDataSource.PeekBlock(trackIndex)) == null) + { + throw new Exception("ASSERT: Reading from a track already at EOF."); + } + // Some kind of time delta for this sample. + timeCode = block0.StartTime - timePosition - minStartTime0; + if (block0.StartTime < timePosition + minStartTime0) + { + throw new Exception("Bad start times: block0.StartTime=" + block0.StartTime + + " timePosition=" + timePosition + " minStartTime=" + minStartTime0); + } + isKeyFrame = block0.IsKeyFrame; + mediaDataBlockTotalSize = block0.Bytes.Count; + if (ungetBlocks[trackIndex] != null) + { + ungetBlocks[trackIndex] = null; + } + else + { + mediaDataSource.ConsumeBlock(trackIndex); + } + } + if (timeCode > 327670000uL) + { + throw new Exception("timeCode too large: " + timeCode); // Maybe that's not fatal? + } + if (isAmsCodecs[trackIndex]) + { // Copy one more MediaSample if available. + // TODO: Test this. + block1 = ungetBlocks[trackIndex]; + if (block1 != null) + { + mediaDataBlockTotalSize += block1.Bytes.Count; + ungetBlocks[trackIndex] = null; + } + else if ((block1 = mediaDataSource.PeekBlock(trackIndex)) != null) + { + mediaDataBlockTotalSize += block1.Bytes.Count; + mediaDataSource.ConsumeBlock(trackIndex); + } + } + // TODO: How can be timeCode so large at this point? + if ((output.Count != 0 && trackIndex == videoTrackIndex && isKeyFrame) || timeCode > 327670000uL) + { + ulong outputOffset = (ulong)fileStream.Position - segmentOffset; + cuePoints.Add(new CuePoint(timePosition / 10000uL, (ulong)(videoTrackIndex + 1), outputOffset)); + muxStateWriter.WriteUlong('C', timePosition); + muxStateWriter.WriteUlong('D', outputOffset); + int totalSize = 0; + for (int i = 0; i < output.Count; ++i) + { + totalSize += output[i].Count; + } + // We do a single copy of the media stream data bytes here. That copy is inevitable, because it's + // faster to save to file that way. + byte[] bytes = Utils.CombineByteArraysAndArraySegments( + new byte[][] { GetDataSizeBytes((ulong)ID.Cluster), GetDataSizeBytes((ulong)totalSize) }, output); + output.Clear(); + // The average bytes.Length is 286834 bytes here, that's large enough (>8 kB), and it doesn't warrant a + // a buffered output stream for speedup. + fileStream.Write(bytes, 0, bytes.Length); + fileStream.Flush(); + for (int i = 0; i < trackCount; ++i) + { + muxStateWriter.WriteUlong((char)('a' + i), lastOutputStartTimes[i]); + } + muxStateWriter.WriteUlong('P', (ulong)bytes.Length); + muxStateWriter.Flush(); + } + if (output.Count == 0) + { + timePosition += timeCode; + timeCode = 0uL; + output.Add(new ArraySegment( + GetEEBytes(ID.Timecode, GetVintBytes(timePosition / 10000uL)))); + } + output.Add(new ArraySegment(GetSimpleBlockBytes( + (ulong)(trackIndex + 1), (short)(timeCode / 10000uL), isKeyFrame, isAmsCodecs[trackIndex], + mediaDataBlockTotalSize))); + output.Add(block0.Bytes); + if (block1 != null) output.Add(block1.Bytes); + lastOutputStartTimes[trackIndex] = block1 != null ? block1.StartTime : block0.StartTime; + } + + // Write remaining samples (from output to fileStream), and write cuePoints. + { + ulong outputOffset = (ulong)fileStream.Position - segmentOffset; + cuePoints.Add(new CuePoint(timePosition / 10000uL, (ulong)(videoTrackIndex + 1), outputOffset)); + muxStateWriter.WriteUlong('C', timePosition); + muxStateWriter.WriteUlong('D', outputOffset); + if (output.Count == 0) + { + throw new Exception("ASSERT: Expecting non-empty output at end of mixing."); + } + int totalSize = 0; + for (int i = 0; i < output.Count; ++i) + { + totalSize += output[i].Count; + } + byte[] bytes = Utils.CombineByteArraysAndArraySegments( + new byte[][] { GetDataSizeBytes((ulong)ID.Cluster), GetDataSizeBytes((ulong)totalSize) }, output); + output.Clear(); // Save memory. + cuesOffsetMS = outputOffset + (ulong)bytes.Length; + byte[] bytes2 = GetCueBytes(cuePoints); // cues are about 1024 bytes per 2 minutes. + seekHeadOffsetMS = cuesOffsetMS + (ulong)bytes2.Length; + SeekBlock[] seekBlocks = new SeekBlock[cuePoints.Count]; + for (int i = 0; i < cuePoints.Count; ++i) + { + seekBlocks[i] = new SeekBlock(ID.Cluster, cuePoints[i].CueClusterPosition); + } + byte[] bytes3 = GetSeekBytes(seekBlocks, -1); + bytes = Utils.CombineBytes(bytes, bytes2, bytes3); + fileStream.Write(bytes, 0, bytes.Length); + } + } + + // Returns trackIndex with the smallest StartTime, or -1. + private static int GetNextTrackIndex(IMediaDataSource mediaDataSource, MediaDataBlock[] ungetBlocks) + { + int trackCount = ungetBlocks.Length; // == mediaDataSource.GetTrackCount(). + ulong minUnconsumedStartTime = 0; // No real need to initialize it here. + int trackIndex = -1; + for (int i = 0; i < trackCount; ++i) + { + MediaDataBlock block = ungetBlocks[i]; + if (block == null) block = mediaDataSource.PeekBlock(i); + if (block != null && (trackIndex == -1 || minUnconsumedStartTime > block.StartTime)) + { + trackIndex = i; + minUnconsumedStartTime = block.StartTime; + } + } + return trackIndex; + } + + private const ulong MUX_STATE_VERSION = 923840374526694867; + + // This is a pure data class, please don't add logic. + private class ParsedMuxState + { + public string status; + public bool hasZ; + public ulong vZ; + public bool hasM; + public ulong vM; + public bool hasS; + public ulong vS; + public bool hasA; + public ulong vA; + public bool isXGood; + public bool hasX; + public ulong vX; + public bool hasV; + public ulong vV; + public bool hasH; + public byte[] vH; + public IList cuePoints; + public bool isComplete; + public bool isContinuable; + public ulong lastOutOfs; + public bool hasC; + public ulong lastC; + // this.trackLastStartTimes[trackIndex] is a StartTime lower limit. When muxing is continued, MediaDataBlock()s with + // .StartTime <= the limit must be ignored (consumed). + public ulong[] trackLastStartTimes; + public IList[] trackChunkStartTimes; + public int endOffset; + public ParsedMuxState() + { + this.isXGood = false; + this.hasZ = this.hasM = this.hasS = this.hasX = this.hasV = this.hasH = this.hasC = this.hasA = false; + this.isComplete = this.isContinuable = false; + this.vZ = this.vM = this.vS = this.vX = this.vV = this.vA = 0; + this.vH = null; + this.status = "unparsed"; + this.cuePoints = null; + this.lastOutOfs = 0; + this.trackLastStartTimes = null; + this.trackChunkStartTimes = null; + this.endOffset = 0; + this.lastC = 0; + } + public override String ToString() + { + StringBuilder buf = new StringBuilder(); + buf.Append("ParsedMuxState(status=" + Utils.EscapeString(this.status)); + if (this.isComplete) + { + buf.Append(", Complete"); + } + else if (this.isContinuable) + { + buf.Append(", Continuable"); + } + else + { + buf.Append(", Unusable"); + } + if (this.isXGood) + { + buf.Append(", XGood"); + } + else if (this.hasX) + { + buf.Append(", X=" + this.vX); + } + if (this.hasS) + { + buf.Append(", S=" + this.vS); + } + if (this.hasH) + { + buf.Append(", H.size=" + this.vH.Length); + } + if (this.hasA) + { + buf.Append(", A=" + this.vA); + } + if (this.hasV) + { + buf.Append(", V=" + this.vV); + } + if (this.hasC) + { + buf.Append(", lastC=" + this.lastC); + } + if (this.hasM) + { + buf.Append(", M=" + this.vM); + } + if (this.hasZ) + { + buf.Append(", Z=" + this.vZ); + } + if (this.cuePoints != null) + { + buf.Append(", cuePoints.size=" + this.cuePoints.Count); + } + if (this.lastOutOfs > 0) + { + buf.Append(", lastOutOfs=" + this.lastOutOfs); + } + if (this.trackLastStartTimes != null) + { + for (int i = 0; i < this.trackLastStartTimes.Length; ++i) + { + buf.Append(", lastStartTime[" + i + "]=" + this.trackLastStartTimes[i]); + } + } + if (this.trackChunkStartTimes != null) + { + for (int i = 0; i < this.trackChunkStartTimes.Length; ++i) + { + buf.Append(", chunkStartTime[" + i + "].Size=" + this.trackChunkStartTimes[i].Count); + } + } + buf.Append(")"); + return buf.ToString(); + } + } + + private static ParsedMuxState ParseMuxState(byte[] muxState, ulong oldSize, byte[] prefix, int prefixSize, + int videoTrackIndex, int trackCount) + { + ParsedMuxState parsedMuxState = new ParsedMuxState(); + if (muxState == null) + { + parsedMuxState.status = "no mux state"; + return parsedMuxState; + } + if (muxState.Length == 0) + { + parsedMuxState.status = "empty mux state"; + return parsedMuxState; + } + if (oldSize == 0) + { + parsedMuxState.status = "empty old file"; + return parsedMuxState; + } + if (prefixSize == 0) + { + parsedMuxState.status = "empty old prefix"; + return parsedMuxState; + } + + // muxState might be truncated, so we find a sensible end offset to parse until. + int end = 0; + byte b; + int i = muxState.Length; + int j; + if (i > 0 && (b = muxState[i - 1]) != '\n' && b != '\r') + { // Ignore the last, incomplete line. + while (i > 0 && (b = muxState[i - 1]) != '\n' && b != '\r') + { + --i; + } + if (i > 0) --i; + } + for (; ; ) + { // Traverse the lines backwards. + while (i > 0 && ((b = muxState[i - 1]) == '\n' || b == '\r')) + { + --i; + } + if (i == 0) break; + j = i; + while (i > 0 && (b = muxState[i - 1]) != '\n' && b != '\r') + { + --i; + } + // Found non-empty line muxState[i : j] (without trailing newlines). + // Console.WriteLine("(" + Encoding.ASCII.GetString(Utils.GetSubBytes(muxState, i, j)) + ")"); + if (muxState[i] == 'Z' || muxState[i] == 'P') + { // Stop just after the last line starting with Z or P. + end = j + 1; // +1 for the trailing newline. + break; + } + } + if (end == 0) + { + parsedMuxState.status = "truncated to useless"; + return parsedMuxState; + } + parsedMuxState.endOffset = end; + + // Parse muxState[:end]. + // Output block state. Values: + // * -5: expecting Z or after Z + // * -4: expecting X, S, H or V + // * -3: expecting A + // * -2: expecting C + // * -1: expecting D + // * 0: expecting a (trackIndex == 0) or M + // * 1: expecting b (trackIndex == 1) + // * ... + // * trackCount: expecting P + int outState = -4; // Output block state: -1 before V, + parsedMuxState.lastC = ulong.MaxValue; + ulong lastD = ulong.MaxValue; + i = 0; + if (i >= end || muxState[i] != 'X') + { + parsedMuxState.status = "expected key X in the beginning"; + return parsedMuxState; + } + while (i < end) + { + byte key = muxState[i++]; + if (key == '\r' || key == '\n') continue; + bool doCheckDup = false; + if (key == 'X' || key == 'S' || key == 'V' || key == 'A' || key == 'M' || key == 'Z' || + key == 'C' || key == 'D' || key == 'P' || (uint)(key - 'a') < 26) + { + ulong v = 0; + while (i < end && (b = muxState[i]) != '\n' && b != '\r') + { + if (((uint)b - '0') > 9) + { + parsedMuxState.status = "expected ulong for key " + (char)key; + return parsedMuxState; + } + if (v > (ulong.MaxValue - (ulong)(b - '0')) / 10) + { + parsedMuxState.status = "ulong overflow for key " + (char)key; + return parsedMuxState; + } + v = 10 * v + (ulong)(b - '0'); + ++i; + } + if (i == end) + { + parsedMuxState.status = "EOF in key " + (char)key; + return parsedMuxState; + } + if (key == 'X' && outState == -4) + { + doCheckDup = parsedMuxState.hasX; parsedMuxState.hasX = true; parsedMuxState.vX = v; + parsedMuxState.isXGood = (v == MUX_STATE_VERSION); + if (!parsedMuxState.isXGood) + { + parsedMuxState.status = "unsupported format version (X)"; + return parsedMuxState; + } + } + else if (key == 'S' && outState == -4) + { + doCheckDup = parsedMuxState.hasS; parsedMuxState.hasS = true; parsedMuxState.vS = v; + } + else if (key == 'V' && outState == -4) + { + doCheckDup = parsedMuxState.hasV; parsedMuxState.hasV = true; parsedMuxState.vV = v; + outState = -3; + } + else if (key == 'A' && outState == -3) + { + doCheckDup = parsedMuxState.hasA; parsedMuxState.hasA = true; parsedMuxState.vA = v; + outState = -2; + } + else if (key == 'M' && outState == 0) + { + doCheckDup = parsedMuxState.hasM; parsedMuxState.hasM = true; parsedMuxState.vM = v; + outState = -5; + } + else if (key == 'Z' && outState == -5) + { + doCheckDup = parsedMuxState.hasZ; parsedMuxState.hasZ = true; parsedMuxState.vZ = v; + } + else if (key == 'C' && outState == -2) + { + outState = -1; + parsedMuxState.lastC = v; + } + else if (key == 'D' && outState == -1) + { + outState = 0; + if (parsedMuxState.cuePoints == null) + { + parsedMuxState.cuePoints = new List(); + } + parsedMuxState.cuePoints.Add(new CuePoint( + parsedMuxState.lastC / 10000uL, (ulong)(videoTrackIndex + 1), v)); + lastD = v; + if (parsedMuxState.trackLastStartTimes == null) + { + parsedMuxState.trackLastStartTimes = new ulong[trackCount]; // Initialized to 0. Good. + } + } + else if ((uint)(key - 'a') < 13 && outState < trackCount && outState == key - 'a') + { + if (v <= parsedMuxState.trackLastStartTimes[outState]) + { + parsedMuxState.status = "trackLastStart time values must increase, got " + v + + ", expected > " + parsedMuxState.trackLastStartTimes[outState]; + return parsedMuxState; + } + parsedMuxState.trackLastStartTimes[outState] = v; + ++outState; + } + else if ((uint)(key - 'n') < 13 && outState >= -3) + { + if (parsedMuxState.trackChunkStartTimes == null) + { + parsedMuxState.trackChunkStartTimes = new IList[trackCount]; + for (int ti = 0; ti < trackCount; ++ti) + { + parsedMuxState.trackChunkStartTimes[ti] = new List(); + } + } + int trackIndex = key - 'n'; + int chunkCount = parsedMuxState.trackChunkStartTimes[trackIndex].Count; + if (chunkCount > 0) + { + ulong lastChunkStartTime = + parsedMuxState.trackChunkStartTimes[trackIndex][chunkCount - 1]; + v += lastChunkStartTime; + if (lastChunkStartTime >= v) + { + parsedMuxState.status = string.Concat(new object[] { + "trackChunkStartTime values must increase, got ", v, ", expected > ", + lastChunkStartTime, " for track ", trackIndex }); + return parsedMuxState; + } + } + parsedMuxState.trackChunkStartTimes[trackIndex].Add(v); + } + else if (key == 'P' && outState == trackCount) + { + outState = -2; + parsedMuxState.lastOutOfs = v + parsedMuxState.vS + lastD; + lastD = ulong.MaxValue; // A placeholder to expose future bugs. + } + else + { + parsedMuxState.status = "unexpected key " + (char)key + " in outState " + outState; + return parsedMuxState; + } + } + else if (key == 'H') + { + if (i == end || muxState[i] != ':') + { + parsedMuxState.status = "expected colon after hex key " + (char)key; + return parsedMuxState; + } + j = ++i; + while (i > 0 && (b = muxState[i]) != '\n' && b != '\r') + { + ++i; + } + byte[] bytes = Utils.HexDecodeBytes(muxState, j, i); + if (bytes == null) + { + parsedMuxState.status = "parse error in hex key " + (char)key; + return parsedMuxState; + } + if (key == 'H' && outState == -4) + { + doCheckDup = parsedMuxState.hasH; parsedMuxState.hasH = true; parsedMuxState.vH = bytes; + } + else + { + parsedMuxState.status = "unexpected key " + (char)key + " in outState " + outState; + return parsedMuxState; + } + } + else + { + parsedMuxState.status = "unknown key " + (char)key; + return parsedMuxState; + } + if (doCheckDup) + { + parsedMuxState.status = "duplicate key " + (char)key; + return parsedMuxState; + } + } + if (outState != -5 && outState != -2) + { + parsedMuxState.status = "unexpected final outState " + outState; + return parsedMuxState; + } + if (!parsedMuxState.hasV) + { + parsedMuxState.status = "missing video track index (V)"; + return parsedMuxState; + } + if (parsedMuxState.vV != (ulong)videoTrackIndex) + { + parsedMuxState.status = "video track index (V) mismatch, expected " + videoTrackIndex; + return parsedMuxState; + } + if (!parsedMuxState.hasH) + { + parsedMuxState.status = "missing hex file prefix (H)"; + return parsedMuxState; + } + if (parsedMuxState.vH.Length < 10) + { + // This shouldn't happen, because we read 4096 bytes below, and the header is usually just 404 bytes long. + parsedMuxState.status = "hex file prefix (H) too short"; + return parsedMuxState; + } + if (parsedMuxState.vH.Length > prefixSize) + { + // This shouldn't happen, because we read 4096 bytes below, and the header is usually just 404 bytes long. + parsedMuxState.status = "hex file prefix (H) too long, maximum prefix size is " + prefixSize; + return parsedMuxState; + } + if (!parsedMuxState.hasS) + { + parsedMuxState.status = "missing segmentOffset (S)"; + return parsedMuxState; + } + if (parsedMuxState.vS < 10 || parsedMuxState.vS > oldSize) + { + parsedMuxState.status = "bad video track index (V) range"; + return parsedMuxState; + } + if (!parsedMuxState.hasA) + { + parsedMuxState.status = "missing minStartTime (A)"; + return parsedMuxState; + } + if (parsedMuxState.hasZ) + { + if (parsedMuxState.vZ != 1) + { + parsedMuxState.status = "bad end marker (Z) value, expected 1"; + return parsedMuxState; + } + if (!parsedMuxState.hasM) + { + parsedMuxState.status = "missing key M"; + return parsedMuxState; + } + if (parsedMuxState.vM != oldSize) + { + parsedMuxState.status = "old file size (M) mismatch, expected " + oldSize; + return parsedMuxState; + } + } + // Console.WriteLine("H(" + Utils.HexEncodeString(Utils.GetSubBytes(prefix, 0, parsedMuxState.vH.Length)) + ")"); + if (!Utils.ArePrefixBytesEqual(parsedMuxState.vH, prefix, parsedMuxState.vH.Length)) + { + // We repeat the comparison with the mediaEndOffsetMS, seekHeadOffsetMS, cuesOffsetMS and duration fields + // ignored. (So compare e.g. the .mkv format version and the track codec parameters.) + // + // If not complete yet (!parsedMuxState.hasZ), then the duration may be different; the other fields may be + // different as well if WriteMkv has written the output of UpdatePrefix, but not the 'Z' value yet. If complete, + // all the fields may be different (usually the duration is the same, and the other fields are different, + // because they don't contain their INITIAL_* value anymore). + // + // We ignore the duration by copying it from one array to the other (it could work the other way around as well). + // We ignore the other fields by setting them back to their INITIAL_* values before the comparison. + int prefixCompareSize = parsedMuxState.vH.Length; // Shortness is checked above. + byte[] prefix1 = new byte[prefixCompareSize]; + Buffer.BlockCopy(parsedMuxState.vH, 0, prefix1, 0, prefixCompareSize); + byte[] prefix2 = new byte[prefixCompareSize]; + Buffer.BlockCopy(prefix, 0, prefix2, 0, prefixCompareSize); + UpdatePrefix( // TODO: Catch exception if this fails. + prefix2, prefixCompareSize, parsedMuxState.vS, + INITIAL_MEDIA_END_OFFSET_MS, INITIAL_SEEK_HEAD_OFFSET_MS, INITIAL_CUES_OFFSET_MS, + KEEP_ORIGINAL_DURATION, /*timeScale:*/0); + int durationOffset; + int afterInfoOffset; // Ignored, dummy. + // TODO: Catch exception if this fails. + FindDurationAndAfterInfoOffset(prefix1, (int)parsedMuxState.vS, prefixCompareSize, + out durationOffset, out afterInfoOffset); + Buffer.BlockCopy(prefix1, durationOffset, prefix2, durationOffset, 4); + if (!Utils.ArePrefixBytesEqual(prefix1, prefix2, prefixCompareSize)) + { + // Console.WriteLine("P(" + Utils.HexEncodeString(Utils.GetSubBytes(prefix, 0, parsedMuxState.vH.Length)) + ")"); + // Console.WriteLine("V(" + Utils.HexEncodeString(Utils.GetSubBytes(parsedMuxState.vH, 0, parsedMuxState.vH.Length)) + ")"); + parsedMuxState.status = "hex file prefix (H) mismatch"; + return parsedMuxState; + } + } + if (parsedMuxState.hasZ) + { + parsedMuxState.isComplete = true; + parsedMuxState.status = "complete"; + } + else if (parsedMuxState.cuePoints == null || parsedMuxState.cuePoints.Count == 0) + { + parsedMuxState.status = "no cue points"; + } + else if (parsedMuxState.lastOutOfs <= parsedMuxState.vS) + { + parsedMuxState.status = "no downloaded media data"; + } + else if (parsedMuxState.lastOutOfs > oldSize) + { + parsedMuxState.status = "file shorter than lastOutOfs"; + } + else if (parsedMuxState.trackChunkStartTimes == null) + { + parsedMuxState.status = "no chunk start times"; + } + else + { + if (parsedMuxState.trackLastStartTimes == null) + { + throw new Exception("ASSERT: expected trackLastStartTimes."); + } + for (i = 0; i < trackCount; ++i) + { + if (parsedMuxState.trackLastStartTimes[i] == 0) + { + throw new Exception("ASSERT: expected positive trackLastStartTimes value."); + } + } + parsedMuxState.isContinuable = true; + parsedMuxState.status = "continuable"; + } + return parsedMuxState; + } + + // This function may modify trackSamples in a destructive way, to save memory. + public static void WriteMkv(string mkvPath, + IList trackEntries, + IMediaDataSource mediaDataSource, + ulong maxTrackEndTimeHint, + ulong timeScale, + bool isDeterministic, + byte[] oldMuxState, + MuxStateWriter muxStateWriter) + { + if (trackEntries.Count != mediaDataSource.GetTrackCount()) + { + throw new Exception("ASSERT: trackEntries vs mediaDataSource length mismatch."); + } + bool doParseOldMuxState = oldMuxState != null && oldMuxState.Length > 0; + FileMode fileMode = doParseOldMuxState ? FileMode.OpenOrCreate : FileMode.Create; + using (FileStream fileStream = new FileStream(mkvPath, fileMode)) + { + ulong oldSize = doParseOldMuxState ? (ulong)fileStream.Length : 0uL; + int videoTrackIndex = GetVideoTrackIndex(trackEntries, 0); + bool isComplete = false; + bool isContinuable = false; + ulong lastOutOfs = 0; + ulong segmentOffset = 0; // Will be overwritten below. + IList cuePoints = null; + ulong minStartTime = 0; + ulong timePosition = ulong.MaxValue; + byte[] prefix = null; // Well be overwritten below. + if (doParseOldMuxState && oldSize > 0) + { + Console.WriteLine("Trying to use the old mux state to continue downloading."); + prefix = new byte[4096]; + int prefixSize = fileStream.Read(prefix, 0, prefix.Length); + ParsedMuxState parsedMuxState = ParseMuxState( + oldMuxState, oldSize, prefix, prefixSize, videoTrackIndex, trackEntries.Count); + if (parsedMuxState.isComplete) + { + Console.WriteLine("The .mkv file is already fully downloaded."); + isComplete = true; + // TODO: Don't even temporarily modify the .muxstate file. + muxStateWriter.WriteRaw(oldMuxState, 0, oldMuxState.Length); + } + else if (parsedMuxState.isContinuable) + { + Console.WriteLine("Continuing the .mkv file download."); + lastOutOfs = parsedMuxState.lastOutOfs; + segmentOffset = parsedMuxState.vS; + cuePoints = parsedMuxState.cuePoints; + minStartTime = parsedMuxState.vA; + timePosition = parsedMuxState.lastC; + // We may save memory after this by trucating prefix to durationOffset + 4 -- but we don't care. + muxStateWriter.WriteRaw(oldMuxState, 0, parsedMuxState.endOffset); + mediaDataSource.StartChunks(new StateChunkStartTimeReceiver( + muxStateWriter, parsedMuxState.trackChunkStartTimes)); + for (int i = 0; i < trackEntries.Count; ++i) + { + // Skip downloading most of the chunk files already in the .mkv (up to lastOutOfs). + mediaDataSource.ConsumeBlocksUntil(i, parsedMuxState.trackLastStartTimes[i]); + } + isContinuable = true; + } + else + { + Console.WriteLine("Could not use old mux state: " + parsedMuxState); + } + } + if (!isComplete) + { + fileStream.SetLength((long)lastOutOfs); + fileStream.Seek((long)lastOutOfs, 0); + if (!isContinuable) + { // Not continuing from previous state, writing an .mkv from scratch. + // EBML: http://matroska.org/technical/specs/rfc/index.html + // http://matroska.org/technical/specs/index.html + prefix = GetEbmlHeaderBytes(); + segmentOffset = (ulong)prefix.Length + 12; + muxStateWriter.WriteUlong('X', MUX_STATE_VERSION); // Unique ID and version number. + muxStateWriter.WriteUlong('S', segmentOffset); // About 52. + prefix = Utils.CombineBytes(prefix, GetSegmentBytes( + /*duration:*/maxTrackEndTimeHint, + INITIAL_MEDIA_END_OFFSET_MS, INITIAL_SEEK_HEAD_OFFSET_MS, INITIAL_CUES_OFFSET_MS, + timeScale, trackEntries, isDeterministic)); + fileStream.Write(prefix, 0, prefix.Length); // Write the MKV header. + fileStream.Flush(); + muxStateWriter.WriteBytes('H', prefix); // About 405 bytes long. + muxStateWriter.WriteUlong('V', (ulong)videoTrackIndex); + cuePoints = new List(); + mediaDataSource.StartChunks(new StateChunkStartTimeReceiver( + muxStateWriter, new IList[trackEntries.Count])); + } + ulong seekHeadOffsetMS; // Will be set by WriteClustersAndCues below. + ulong cuesOffsetMS; // Will be set by WriteClustersAndCues below. + WriteClustersAndCues( + fileStream, segmentOffset, videoTrackIndex, GetIsAmsCodecs(trackEntries), + mediaDataSource, muxStateWriter, cuePoints, ref minStartTime, timePosition, + out seekHeadOffsetMS, out cuesOffsetMS); + fileStream.Flush(); + // Update the MKV header with the file size. + ulong mediaEndOffset = (ulong)fileStream.Position; + muxStateWriter.WriteUlong('M', mediaEndOffset); + // Usually this seek position is 45. + ulong maxTrackEndTime = 0; // TODO: mkvmerge calculates this differently (<0.5s -- rounding?) + for (int i = 0; i < mediaDataSource.GetTrackCount(); ++i) + { + ulong trackEndTime = mediaDataSource.GetTrackEndTime(i); + if (maxTrackEndTime < trackEndTime) maxTrackEndTime = trackEndTime; + } + // Update the ID.Segment size and ID.Duration with their final values. + int seekOffset = (int)segmentOffset - 7; + // We update the final duration and some offsets in the .mkv header so mplayer (and possibly other + // media players) will be able to seek in the file without additional tricks. More specifically: + // + // play-before play-after seek-before seek-after + // mplayer yes yes no yes + // mplayer -idx yes yes yes yes + // mplayer2 yes yes yes yes + // mplayer2 -idx yes yes yes yes + // VLC 1.0.x no no no no + // VLC 1.1.x no no no no + // VLC 2.0.x ? yes ? yes + // SMPlayer 0.6.9 ? yes ? yes + // + // Legend: + // + // * mplayer: MPlayer SVN-r1.0~rc3+svn20090426-4.4.3 on Ubuntu Lucid + // * mplayer2: MPlayer2 2.0 from http://ftp.mplayer2.org/pub/release/ , mtime 2011-03-26 + // * -idx; The -idx command-line flag of mplayer and mplayer2. + // * play: playing the video sequentially from beginning to end + // * seek: jumping back and forth within the video upon user keypress (e.g. the key), + // including jumping to regions of the .mkv which haven't been downloaded when playback started + // * before: before running UpdatePrefix below, i.e. while the .mkv is being downloaded + // * after: after running UpdatePrefix below + // + // VLC 1.0.x and VLC 1.1.x problems: audio is fine, but the video is jumping back and forth fraction of + // a second. + int updateOffset = UpdatePrefix( + prefix, prefix.Length, segmentOffset, + mediaEndOffset - segmentOffset, + /*seekHeadOffsetMS:*/seekHeadOffsetMS, + /*cuesOffsetMS:*/cuesOffsetMS, + /*duration:*/maxTrackEndTime - minStartTime, timeScale); + fileStream.Seek(seekOffset, 0); + fileStream.Write(prefix, seekOffset, updateOffset - seekOffset); + fileStream.Flush(); + muxStateWriter.WriteUlong('Z', 1); + muxStateWriter.Flush(); + } + } + } + } +} diff --git a/src/Mp4.cs b/src/Mp4.cs new file mode 100644 index 0000000..659b0ea --- /dev/null +++ b/src/Mp4.cs @@ -0,0 +1,462 @@ +using System; +using System.IO; +using System.Collections.Generic; +namespace Smoothget.Mp4 +{ + // The 4 bytes (MSB first) of these values correspond to the 4 bytes of the name (e.g. 'f', 't', 'y', p' for ftyp). + public enum ID : uint + { + uuid = 1970628964u, + sdtp = 1935963248u, + moof = 1836019558u, + mfhd = 1835427940u, + traf = 1953653094u, + tfhd = 1952868452u, + trun = 1953658222u, + mdat = 1835295092u, + unsupported = 0u, + tfrf = 1u, + tfxd = 2u, + min = 100u, + } + + public class Fragment + { + public MovieFragmentBox moof; + public MediaDataBox mdat; + public Fragment(byte[] boxBytes, int start, int end) + { + while (start < end) + { + Box box = Mp4Utils.GetBox(boxBytes, ref start, end); + if (box == null) + { + } + else if (box.ID == ID.mdat) + { + this.mdat = (box as MediaDataBox); + } + else if (box.ID == ID.moof) + { + this.moof = (box as MovieFragmentBox); + } + } + } + } + + public class Box + { + public ID ID; + public Box(ID id) + { + this.ID = id; + } + } + + public class MediaDataBox : Box + { + public int Start; + public MediaDataBox(byte[] boxBytes, int start, int end) + : base(ID.mdat) + { + this.Start = start; + } + } + + public class MovieFragmentHeaderBox : Box + { + public uint SequenceNumber; + public MovieFragmentHeaderBox(byte[] boxBytes, int start, int end) + : base(ID.mfhd) + { + if (end - start != 8) + { + throw new Exception("Invalid '" + base.ID + "' length!"); + } + start += 4; + this.SequenceNumber = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + } + + public class MovieFragmentBox : Box + { + public MovieFragmentHeaderBox mfhd; + public TrackFragmentBox traf; + public MovieFragmentBox(byte[] boxBytes, int start, int end) + : base(ID.moof) + { + while (start < end) + { + Box box = Mp4Utils.GetBox(boxBytes, ref start, end); + if (box == null) + { + } + else if (box.ID == ID.traf) + { + this.traf = (box as TrackFragmentBox); + } + else if (box.ID == ID.mfhd) + { + this.mfhd = (box as MovieFragmentHeaderBox); + } + } + } + } + + public class TrackFragmentBox : Box + { + public TrackFragmentHeaderBox tfhd; + public TrackRunBox trun; + public SampleDependencyTypeBox sdtp; + public TfrfBox tfrf; + public TfxdBox tfxd; + public TrackFragmentBox(byte[] boxBytes, int start, int end) + : base(ID.traf) + { + while (start < end) + { + Box box = Mp4Utils.GetBox(boxBytes, ref start, end); + if (box != null) + { + ID iD = box.ID; + if (iD == ID.tfhd) + { + this.tfhd = (box as TrackFragmentHeaderBox); + } + else if (iD == ID.sdtp) + { + this.sdtp = (box as SampleDependencyTypeBox); + } + else if (iD == ID.trun) + { + this.trun = (box as TrackRunBox); + } + else if (iD == ID.tfrf) + { + this.tfrf = (box as TfrfBox); + } + else if (iD == ID.tfxd) + { + this.tfxd = (box as TfxdBox); + } + } + } + } + } + + public class TfxdBox : Box + { + public byte Version; + public byte[] Flags; + public ulong FragmentAbsoluteTime; + public ulong FragmentDuration; + public TfxdBox(byte[] boxBytes, int start, int end) + : base(ID.tfxd) + { + // TODO: Don't read and populate unused fields. + this.Version = Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; + this.Flags = Mp4Utils.ReadReverseBytes(boxBytes, 3, ref start, end); + if (this.Version == 0) + { + this.FragmentAbsoluteTime = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes( + boxBytes, 4, ref start, end), 0); + this.FragmentDuration = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + else + { + if (this.Version != 1) + { + throw new Exception("Invalid TfxdBox version '" + this.Version + "'!"); + } + this.FragmentAbsoluteTime = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); + this.FragmentDuration = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); + } + } + } + + public class SampleDependencyTypeBox : Box + { + public class Element + { + public byte reserved; + public byte sample_depends_on; + public byte sample_is_depended_on; + public byte sample_has_redundancy; + public Element(byte reserved, byte sample_depends_on, byte sample_is_depended_on, byte sample_has_redundancy) + { + this.reserved = reserved; + this.sample_depends_on = sample_depends_on; + this.sample_is_depended_on = sample_is_depended_on; + this.sample_has_redundancy = sample_has_redundancy; + } + } + public uint version; + public SampleDependencyTypeBox.Element[] array; + public SampleDependencyTypeBox(byte[] boxBytes, int start, int end) + : base(ID.sdtp) + { + this.version = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + int count = end - start; + this.array = new SampleDependencyTypeBox.Element[count]; + for (int i = 0; i < count; i++) + { + byte b = Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; + byte reserved = (byte)(b >> 6); + b <<= 2; + byte sample_depends_on = (byte)(b >> 6); + b <<= 2; + byte sample_is_depended_on = (byte)(b >> 6); + b <<= 2; + byte sample_has_redundancy = (byte)(b >> 6); + this.array[i] = new SampleDependencyTypeBox.Element( + reserved, sample_depends_on, sample_is_depended_on, sample_has_redundancy); + } + } + } + + public class TfrfBox : Box + { + public class Element + { + public ulong FragmentAbsoluteTime; + public ulong FragmentDuration; + public Element(byte[] boxBytes, byte version, ref int start, int end) + { + if (version == 0) + { + this.FragmentAbsoluteTime = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + this.FragmentDuration = (ulong)BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + else + { + if (version != 1) + { + throw new Exception("Invalid TfrfBox version '" + version + "'!"); + } + this.FragmentAbsoluteTime = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); + this.FragmentDuration = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); + } + } + } + public byte Version; + public byte[] Flags; + public Element[] Array; + public TfrfBox(byte[] boxBytes, int start, int end) + : base(ID.tfrf) + { + this.Version = Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; + this.Flags = Mp4Utils.ReadReverseBytes(boxBytes, 3, ref start, end); + int fragmentCount = (int)Mp4Utils.ReadReverseBytes(boxBytes, 1, ref start, end)[0]; + this.Array = new TfrfBox.Element[fragmentCount]; + for (int i = 0; i < fragmentCount; i++) + { + this.Array[i] = new TfrfBox.Element(boxBytes, this.Version, ref start, end); + } + // TODO: Do we want to test start == end (in other classes as well?). + } + } + + public class TrackFragmentHeaderBox : Box + { + public uint tf_flags; + public uint track_ID; + public ulong base_data_offset; + public uint sample_description_index; + public uint default_sample_duration; + public uint default_sample_size; + public uint default_sample_flags; + public bool base_data_offset_present; + public bool sample_description_index_present; + public bool default_sample_duration_present; + public bool default_sample_size_present; + public bool default_sample_flags_present; + public bool duration_is_empty; + public TrackFragmentHeaderBox(byte[] boxBytes, int start, int end) + : base(ID.tfhd) + { + // TODO: Don't store field this.tr_flags etc. if not used. + this.tf_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + this.base_data_offset_present = ((1u & this.tf_flags) != 0u); + this.sample_description_index_present = ((2u & this.tf_flags) != 0u); + this.default_sample_duration_present = ((8u & this.tf_flags) != 0u); + this.default_sample_size_present = ((16u & this.tf_flags) != 0u); + this.default_sample_flags_present = ((32u & this.tf_flags) != 0u); + this.duration_is_empty = ((65536u & this.tf_flags) != 0u); + this.track_ID = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + if (this.base_data_offset_present) + { + this.base_data_offset = BitConverter.ToUInt64(Mp4Utils.ReadReverseBytes(boxBytes, 8, ref start, end), 0); + } + if (this.sample_description_index_present) + { + this.sample_description_index = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes( + boxBytes, 4, ref start, end), 0); + } + if (this.default_sample_duration_present) + { + this.default_sample_duration = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes( + boxBytes, 4, ref start, end), 0); + } + if (this.default_sample_size_present) + { + this.default_sample_size = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + if (this.default_sample_flags_present) + { + this.default_sample_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + } + } + + public class TrackRunBox : Box + { + public struct Element + { + public uint sample_duration; + public uint sample_size; + public uint sample_flags; + public uint sample_composition_time_offset; + public Element(uint sample_duration, uint sample_size, uint sample_flags, uint sample_composition_time_offset) + { + this.sample_duration = sample_duration; + this.sample_size = sample_size; + this.sample_flags = sample_flags; + this.sample_composition_time_offset = sample_composition_time_offset; + } + } + public uint tr_flags; + public uint sample_count; + public int data_offset; + public uint first_sample_flags; + public TrackRunBox.Element[] array; + public bool data_offset_present; + public bool first_sample_flags_present; + public bool sample_duration_present; + public bool sample_size_present; + public bool sample_flags_present; + public bool sample_composition_time_offsets_present; + public TrackRunBox(byte[] boxBytes, int start, int end) + : base(ID.trun) + { + // TODO: Don't store field this.tr_flags etc. if not used. + this.tr_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + this.data_offset_present = ((1u & this.tr_flags) != 0u); + this.first_sample_flags_present = ((4u & this.tr_flags) != 0u); + this.sample_duration_present = ((256u & this.tr_flags) != 0u); + this.sample_size_present = ((512u & this.tr_flags) != 0u); + this.sample_flags_present = ((1024u & this.tr_flags) != 0u); + this.sample_composition_time_offsets_present = ((2048u & this.tr_flags) != 0u); + this.sample_count = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + if (this.data_offset_present) + { + this.data_offset = BitConverter.ToInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + if (this.first_sample_flags_present) + { + this.first_sample_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + this.array = new TrackRunBox.Element[this.sample_count]; + for (int i = 0; i < this.array.Length; i++) + { + uint sample_duration = 0u; + uint sample_size = 0u; + uint sample_flags = 0u; + uint sample_composition_time_offset = 0u; + if (this.sample_duration_present) + { + sample_duration = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + if (this.sample_size_present) + { + sample_size = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + if (this.sample_flags_present) + { + sample_flags = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + if (this.sample_composition_time_offsets_present) + { + sample_composition_time_offset = BitConverter.ToUInt32(Mp4Utils.ReadReverseBytes(boxBytes, 4, ref start, end), 0); + } + this.array[i] = new TrackRunBox.Element(sample_duration, sample_size, sample_flags, sample_composition_time_offset); + } + } + } + + internal class Mp4Utils + { + public static byte[] ReadReverseBytes(byte[] boxBytes, int count, ref int start, int end) + { + if (start + count > end) + { + throw new Exception("Short read, wanted " + count); + } + byte[] array = new byte[count]; + Buffer.BlockCopy(boxBytes, start, array, 0, count); + start += count; + Array.Reverse(array); + return array; + } + public static Box GetBox(byte[] boxBytes, ref int start, int end) + { + // TODO: Integrate ReadReverseBytes and BitConverter.ToUint*. + int length = BitConverter.ToInt32(ReadReverseBytes(boxBytes, 4, ref start, end), 0); + uint idNum = BitConverter.ToUInt32(ReadReverseBytes(boxBytes, 4, ref start, end), 0); + if (length == 1) + { + // TODO: Test this. + length = (int)BitConverter.ToUInt64(ReadReverseBytes(boxBytes, 8, ref start, end), 0) - 16; + } + else if (length == 0) + { + length = end - start; // TODO: `offset' seems to be correct. Test this. + } + else + { + length -= 8; + } + if (length < 0) + { + throw new Exception("Length too small."); + } + int contentStart = start; + start += length; + if (start > end) + { + throw new Exception("Box '" + idNum + "' ends outside the file!"); + } + switch (idNum) + { + case (uint)ID.mdat: { return new MediaDataBox(boxBytes, contentStart, start); } + case (uint)ID.mfhd: { return new MovieFragmentHeaderBox(boxBytes, contentStart, start); } + case (uint)ID.moof: { return new MovieFragmentBox(boxBytes, contentStart, start); } + case (uint)ID.sdtp: { return new SampleDependencyTypeBox(boxBytes, contentStart, start); } + case (uint)ID.tfhd: { return new TrackFragmentHeaderBox(boxBytes, contentStart, start); } + case (uint)ID.traf: { return new TrackFragmentBox(boxBytes, contentStart, start); } + case (uint)ID.trun: { return new TrackRunBox(boxBytes, contentStart, start); } + case (uint)ID.uuid: + { + Guid uUID = new Guid(ReadReverseBytes(boxBytes, 8, ref contentStart, start)); + if (uUID == TfxdGuid) + { + return new TfxdBox(boxBytes, contentStart, start); + } + else if (uUID == TfrfGuid) + { + return new TfrfBox(boxBytes, contentStart, start); + } + else if (uUID == PiffGuid) + { + throw new Exception("DRM protected data!"); + } + break; + } + } + return null; + } + private static readonly Guid TfxdGuid = new Guid("6D1D9B05-42D5-44E6-80E2-141DAFF757B2"); + private static readonly Guid TfrfGuid = new Guid("D4807EF2-CA39-4695-8E54-26CB9E46A79F"); + // mp4parser.boxes.piff.PiffSampleEncryptionBox + private static readonly Guid PiffGuid = new Guid("A2394F52-5A9B-4F14-A244-6C427C648DF4"); + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..9fddac3 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,261 @@ +using Smoothget.Download; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; + +//[assembly: AssemblyVersion("3.0.0.3")] + +namespace Smoothget +{ + internal interface IUrlProcessor + { + // Processes inputUrls sequentially, appending the results to outputUrls. + void Process(IList inputUrls, IList outputUrls); + int GetOrder(); + } + + internal class ManifestUrlProcessor : IUrlProcessor + { + /*implements*/ + public void Process(IList inputUrls, IList outputUrls) + { + foreach (string url in inputUrls) + { + if (url.EndsWith("/manifest") || url.EndsWith("/Manifest")) + { + outputUrls.Add(url.Substring(0, url.Length - 9)); // "/Manifest".Length == 9. + } + else + { + outputUrls.Add(url); + } + } + } + /*implements*/ + public int GetOrder() { return 999; } + } + + internal class MainClass + { + private static void Main(string[] args) + { + Logo(); + int j; + bool isDeterministic = false; + for (j = 0; j < args.Length; ++j) + { + if (args[j] == "--") + { + ++j; + break; + } + else if (args[j].Length == 0 || args[j] == "-" || args[j][0] != '-') + { + break; + } + else if (args[j] == "--det") + { + isDeterministic = true; + } + } + if (args.Length < j + 2) + { + Help(); + } + int lastIdx = args.Length - 1; + string downloadDirectory = args[lastIdx].Trim(Path.GetInvalidFileNameChars()).Trim(Path.GetInvalidPathChars()); + Console.WriteLine("Download directory: " + downloadDirectory); + string[] urls = new string[lastIdx - j]; + for (int i = j; i < lastIdx; ++i) + { + urls[i - j] = args[i]; + } + IList partUrls = ProcessUrls(urls); + Console.WriteLine("Parts to download:"); + for (int i = 0; i < partUrls.Count; i++) + { + Console.WriteLine(" Part URL: " + partUrls[i]); + } + Console.WriteLine(); + for (int i = 0; i < partUrls.Count; i++) + { + RecordAndMux(partUrls[i], downloadDirectory, isDeterministic); + } + Console.WriteLine("All downloading and muxing done."); + } + + // May return the same reference (urls). + private static IList ProcessUrls(IList urls) + { + Type urlProcessorType = typeof(IUrlProcessor); + List urlProcessors = new List(); + foreach (Type type in urlProcessorType.Assembly.GetTypes()) + { + if (type.IsClass && urlProcessorType.IsAssignableFrom(type)) + { + urlProcessors.Add((IUrlProcessor)type.GetConstructor(Type.EmptyTypes).Invoke(null)); + } + } + urlProcessors.Sort(CompareUrlProcessorOrder); + foreach (IUrlProcessor urlProcessor in urlProcessors) + { + List nextUrls = new List(); + urlProcessor.Process(urls, nextUrls); + urls = nextUrls; + } + return urls; + } + + private static int CompareUrlProcessorOrder(IUrlProcessor a, IUrlProcessor b) + { + return a.GetOrder().CompareTo(b.GetOrder()); + } + + // A thin wrapper for callbacks of duration progress reporting and stopping. + private class MuxingInteractiveState + { + private bool hasDisplayedDuration; + private ulong startTicks; + private ulong reachedBaseTicks; + private Thread thread; + private IStoppable stoppable; + public MuxingInteractiveState() + { + this.hasDisplayedDuration = false; + this.startTicks = 0; + } + public void SetupStop(bool isLive, IStoppable stoppable) + { + if (this.thread != null) + { + throw new Exception("ASSERT: Unexpected thread."); + } + if (isLive) + { + this.stoppable = stoppable; + Console.WriteLine("Press any key to stop recording!"); + this.thread = new Thread(new ThreadStart(StopRecoding)); + this.thread.Start(); + } + } + public void Abort() + { + if (this.thread != null) this.thread.Abort(); + } + public void StopRecoding() + { // Runs in a separate thread parallel with DownloadAndMux. + Console.ReadKey(true); + this.stoppable.Stop(); + } + public void DisplayDuration(ulong reachedTicks, ulong totalTicks) + { + if (!this.hasDisplayedDuration) + { + this.hasDisplayedDuration = true; + Console.Error.WriteLine("Recording duration:"); + } + ulong eta = 0; + if (reachedTicks > 0) + { + ulong nowTicks = (ulong)DateTime.Now.Ticks; + if (this.startTicks == 0) + { + this.startTicks = nowTicks; + this.reachedBaseTicks = reachedTicks; + } + else + { + // TODO: Improve the ETA calculation, it seems to be jumping up and down. + // Here nowTicks and this.startTicks are measured in real time. + // Here totalTicks, reachedTicks and this.reachedBaseTicks are measured in video + // timecode. + double etaDouble = (nowTicks - this.startTicks + 0.0) * + (totalTicks - reachedTicks) / (reachedTicks - this.reachedBaseTicks); + if (etaDouble > 0.0 && etaDouble < 3.6e12) eta = (ulong)etaDouble; // < 100 hours. + } + } + // TODO: Use a StringBuilder. + string msg = "\r" + new TimeSpan((long)(reachedTicks - reachedTicks % 10000000)); + if (eta != 0) + { + msg += ", ETA " + new TimeSpan((long)(eta - eta % 10000000)); // Round down to whole seconds. + } + else + { + msg += " "; // Clear the end (ETA) of the previously displayed message. + } + Console.Write(msg); + } + } + private static void RecordAndMux(string ismFileName, string outputDirectory, bool isDeterministic) + { + string mkvPath = outputDirectory + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(ismFileName) + ".mkv"; + string muxStatePath = Path.ChangeExtension(mkvPath, "muxstate"); + if (File.Exists(mkvPath) && !File.Exists(muxStatePath)) + { + Console.WriteLine("Already downloaded MKV: " + mkvPath); + Console.WriteLine(); + return; + } + Console.WriteLine("Will mux to MKV: " + mkvPath); + string manifestUrl = ismFileName + "/manifest"; + Uri manifestUri; + string manifestPath; + if (manifestUrl.StartsWith("http://") || manifestUrl.StartsWith("https://")) + { + manifestUri = new Uri(manifestUrl); + manifestPath = null; + } + else if (manifestUrl.StartsWith("file://")) + { + // Uri.LocalPath converts %20 back to a space etc. (unlike Uri.AbsolutePath). + // TODO: Does Uri.LocalPath work on Windows (drive letters, / to \ etc.)? + manifestUri = null; + manifestPath = new Uri(manifestUrl).LocalPath; + } + else + { + manifestUri = null; + manifestPath = manifestUrl; + } + DateTime now = DateTime.Now; + MuxingInteractiveState muxingInteractiveState = new MuxingInteractiveState(); + Downloader.DownloadAndMux(manifestUri, manifestPath, mkvPath, isDeterministic, + new TimeSpan(10, 0, 0), // 10 hours, 0 minutes, 0 seconds for live streams. + muxingInteractiveState.SetupStop, + muxingInteractiveState.DisplayDuration); + muxingInteractiveState.Abort(); + Console.Error.WriteLine(); + Console.WriteLine("Downloading finished in " + DateTime.Now.Subtract(now).ToString()); + } + private static void Logo() + { + AssemblyName name = Assembly.GetEntryAssembly().GetName(); + Console.WriteLine(string.Concat(new object[] { name.Name, " v", name.Version })); + Console.WriteLine(); + } + private static void Help() + { + // Console.WriteLine(" Soda Media Center"); // TODO: Where does it come crom? + AssemblyName name = Assembly.GetEntryAssembly().GetName(); + Console.WriteLine("Microsoft IIS Smooth Streaming downloader and muxer to MKV."); + Console.WriteLine(); + Console.WriteLine("Supported media stream formats:"); // TODO: Really only these? + Console.WriteLine("- Audio: AAC, WMA"); + Console.WriteLine("- Video: H264, VC-1"); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(name.Name + " [ ...] [...] "); + Console.WriteLine(" is an .ism (or /manifest) file or URL."); + Console.WriteLine(" can be just . , a properly named file will be created."); + Console.WriteLine("Many temporary files and subdirs may be created and left in ."); + Console.WriteLine("--det Enable deterministic MKV output (no random, no current time)."); + Console.WriteLine("--aq Audio quality level (from zero, sorted from best to lower quality)."); + Console.WriteLine("--vq Video quality level (from zero, sorted from best to lower quality)."); + Environment.Exit(1); + } + } +} diff --git a/src/Smooth.cs b/src/Smooth.cs new file mode 100644 index 0000000..d82b2e0 --- /dev/null +++ b/src/Smooth.cs @@ -0,0 +1,829 @@ +using Smoothget; +using Smoothget.Mkv; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; +namespace Smoothget.Smooth +{ + public enum MediaStreamType + { + Audio, + Video, + Script + } + + internal class ManifestInfo + { + public uint MajorVersion; + public uint MinorVersion; + public ulong Duration; + public bool IsLive; + public ulong TimeScale; + public Uri Uri; + public IDictionary Attributes; + public IList AvailableStreams; + public IList SelectedStreams; + public ulong TotalTicks; + private ManifestInfo(XmlNode element, Uri uri) + { + this.Uri = uri; + if (element.Name != "SmoothStreamingMedia") + { + throw new Exception("Source element is not a(n) SmoothStreamingMedia element!"); + } + this.Attributes = Parse.Attributes(element); + this.MajorVersion = Parse.UInt32Attribute(this.Attributes, "MajorVersion"); + this.MinorVersion = Parse.UInt32Attribute(this.Attributes, "MinorVersion"); + this.Duration = Parse.UInt64Attribute(this.Attributes, "Duration"); + this.IsLive = false; + if (this.Attributes.ContainsKey("IsLive")) + { + this.IsLive = Parse.BoolAttribute(this.Attributes, "IsLive"); + } + this.TimeScale = 10000000uL; + if (this.Attributes.ContainsKey("TimeScale")) + { + this.TimeScale = Parse.UInt64Attribute(this.Attributes, "TimeScale"); + } + this.AvailableStreams = new List(); + foreach (XmlNode element2 in element.SelectNodes("StreamIndex")) + { // Automatic cast to XmlNode. + this.AvailableStreams.Add(new StreamInfo(element2, this.Uri)); + } + this.SelectedStreams = new List(); + for (int i = 0; i < this.AvailableStreams.Count; i++) + { + if (this.AvailableStreams[i].Type != MediaStreamType.Script) + { + this.SelectedStreams.Add(this.AvailableStreams[i]); + } + } + this.TotalTicks = this.IsLive ? 0 : (ulong)(this.Duration / this.TimeScale * 10000000.0); + } + public static ManifestInfo ParseManifest(Stream manifestStream, Uri manifestUri) + { + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.Load(manifestStream); + XmlNode xmlNode = xmlDocument.SelectSingleNode("SmoothStreamingMedia"); + if (xmlNode == null) + { + throw new Exception(string.Format("Manifest root element must be <{0}>!", "SmoothStreamingMedia")); + } + return new ManifestInfo(xmlNode, manifestUri); + } + public string GetDescription() + { + string text = ""; // TODO: Use something like a StringBuilder to avoid O(n^2) concatenation. + text += "Manifest:\n"; + text += " Duration: "; + if (this.IsLive) + { + text += "LIVE\n"; + } + else + { + text += new TimeSpan((long)this.TotalTicks) + "\n"; + } + for (int i = 0; i < this.AvailableStreams.Count; i++) + { + object obj = text; + text = string.Concat(new object[] { obj, " Stream ", i + 1, ": ", this.AvailableStreams[i].Type, "\n" }); + switch (this.AvailableStreams[i].Type) + { + case MediaStreamType.Audio: + case MediaStreamType.Video: + { + foreach (TrackInfo trackInfo in this.AvailableStreams[i].AvailableTracks) + { + text += " " + trackInfo.Description; + if (this.AvailableStreams[i].SelectedTracks.Contains(trackInfo)) + { + text += " [selected]"; + } + text += "\n"; + } + break; + } + case MediaStreamType.Script: + { + text += " Script ignored.\n"; + break; + } + default: + { + text += " WARNING: Unsupported track of type " + this.AvailableStreams[i].Type + "\n"; + break; + } + } + } + return text; + } + } + + internal class StreamInfo + { + private string pureUrl; + public IDictionary Attributes; + public IDictionary CustomAttributes; + public IList AvailableTracks; + public IList SelectedTracks; + public IList ChunkList; + public string Subtype; + public MediaStreamType Type; + public int ChunkCount; + public StreamInfo(XmlNode element, Uri manifestUri) + { + if (element.Name != "StreamIndex") + { + throw new Exception("Source element is not a(n) StreamIndex element!"); + } + this.Attributes = Parse.Attributes(element); + this.CustomAttributes = Parse.CustomAttributes(element); + this.Type = Parse.MediaStreamTypeAttribute(this.Attributes, "Type"); + this.Subtype = this.Attributes.ContainsKey("Subtype") ? Parse.StringAttribute(this.Attributes, "Subtype") : ""; + if (this.Attributes.ContainsKey("Url")) + { + this.CheckUrlAttribute(); + } + this.AvailableTracks = new List(); + XmlNodeList xmlNodeList = element.SelectNodes("QualityLevel"); + int i; + for (i = 0; i < xmlNodeList.Count; ++i) + { + TrackInfo trackInfo; + if (this.Type == MediaStreamType.Audio) + { + trackInfo = new AudioTrackInfo(xmlNodeList[i], this.Attributes, (uint)i, this); + } + else if (this.Type == MediaStreamType.Video) + { + trackInfo = new VideoTrackInfo(xmlNodeList[i], this.Attributes, (uint)i, this); + } + else + { + continue; + } + int num = 0; + while (num < this.AvailableTracks.Count && this.AvailableTracks[num].Bitrate > trackInfo.Bitrate) + { + num++; + } + this.AvailableTracks.Insert(num, trackInfo); + } + this.ChunkList = new List(); + XmlNodeList xmlNodeList2 = element.SelectNodes("c"); + ulong num2 = 0uL; + for (i = 0; i < xmlNodeList2.Count; i++) + { + ChunkInfo chunkInfo = new ChunkInfo(xmlNodeList2[i], (uint)i, num2); + this.ChunkList.Add(chunkInfo); + num2 += chunkInfo.Duration; + } + if (this.Attributes.ContainsKey("Chunks")) + { + uint chunkCount = Parse.UInt32Attribute(this.Attributes, "Chunks"); + if (this.ChunkList.Count > 0 && this.ChunkList.Count != chunkCount) + { + throw new Exception("Chunk count mismatch: c=" + this.ChunkList.Count + " chunks=" + chunkCount); + } + this.ChunkCount = (int)chunkCount; + } + else + { + this.ChunkCount = this.ChunkList.Count; // Can be 0 if no `(); + + if (this.AvailableTracks.Count > 0) + { + this.SelectedTracks.Add(this.AvailableTracks[0]); + } + } + private void CheckUrlAttribute() + { + string text = Parse.StringAttribute(this.Attributes, "Url"); + string[] array = text.Split(new char[] { + '/' + }); + if (array.Length != 2) + { + throw new Exception("Invalid UrlPattern!"); + } + string text2 = array[0]; + string text3 = array[1]; + array = text2.Split(new char[] { + '(', + ')' + }); + if (array.Length != 3 || array[2].Length != 0) + { + throw new Exception("Invalid QualityLevelsPattern!"); + } + if (array[0] != "QualityLevels") + { + throw new Exception("Invalid QualityLevelsNoun!"); + } + string text4 = array[1]; + array = text4.Split(new char[] { + ',' + }); + if (array.Length > 2) + { + throw new Exception("Invalid QualityLevelsPredicatePattern!"); + } + if (array[0] != "{bitrate}" && array[0] != "{Bitrate}") + { + throw new Exception("Missing BitrateSubstitution!"); + } + if (array.Length == 2 && array[1] != "{CustomAttributes}") + { + throw new Exception("Missing CustomAttributesSubstitution!"); + } + array = text3.Split(new char[] { + '(', + ')' + }); + if (array.Length != 3 || array[2].Length != 0) + { + throw new Exception("Invalid FragmentsPattern!"); + } + if (array[0] != "Fragments") + { + throw new Exception("Invalid FragmentsNoun!"); + } + string text5 = array[1]; + array = text5.Split(new char[] { + '=' + }); + if (array.Length != 2) + { + throw new Exception("Invalid FragmentsPatternPredicate!"); + } + if (this.Attributes.ContainsKey("Name")) + { + if (array[0] != Parse.StringAttribute(this.Attributes, "Name")) + { + throw new Exception("Missing TrackName!"); + } + } + else + { + if (array[0] != Parse.StringAttribute(this.Attributes, "Type")) + { + throw new Exception("Missing TrackName!"); + } + } + if (array[1] != "{start time}" && array[1] != "{start_time}") + { + throw new Exception("Missing StartTimeSubstitution!"); + } + } + public string GetChunkUrl(uint bitrate, ulong startTime) + { + return this.pureUrl + "/" + Parse.StringAttribute(this.Attributes, "Url") + .Replace("{bitrate}", bitrate.ToString()) + .Replace("{Bitrate}", bitrate.ToString()) + .Replace("{start time}", startTime.ToString()) + .Replace("{start_time}", startTime.ToString()); + } + } + + internal class TrackInfo + { + public IDictionary Attributes; + public uint Bitrate; + public IDictionary CustomAttributes; + public uint Index; + public StreamInfo Stream; + public string Description; + public TrackEntry TrackEntry; + public TrackInfo(XmlNode element, uint index, StreamInfo stream) + { + if (element.Name != "QualityLevel") + { + throw new Exception("Source element is not a(n) QualityLevel element!"); + } + this.Attributes = Parse.Attributes(element); + this.CustomAttributes = Parse.CustomAttributes(element); + this.Index = index; + if (this.Attributes.ContainsKey("Index")) + { + this.Index = Parse.UInt32Attribute(this.Attributes, "Index"); + } + if (this.Index != index) + { + throw new Exception("Missing quality level index: " + index); + } + this.Bitrate = Parse.UInt32Attribute(this.Attributes, "Bitrate"); + this.Stream = stream; + } + } + + internal class ChunkInfo + { + public uint Index; + public ulong Duration; + public ulong StartTime; + public IDictionary Attributes; + public ChunkInfo(XmlNode element, uint index, ulong starttime) + { + if (element.Name != "c") + { + throw new Exception("Source element is not a(n) c element!"); + } + this.Attributes = Parse.Attributes(element); + this.Index = index; + if (this.Attributes.ContainsKey("n")) + { + this.Index = Parse.UInt32Attribute(this.Attributes, "n"); + } + if (this.Index != index) + { + throw new Exception("Missing chunk index: " + index); + } + this.StartTime = starttime; + if (this.Attributes.ContainsKey("t")) + { + this.StartTime = Parse.UInt64Attribute(this.Attributes, "t"); + } + if (this.Attributes.ContainsKey("d")) + { + this.Duration = Parse.UInt64Attribute(this.Attributes, "d"); + } + } + } + + internal class AudioTrackInfo : TrackInfo + { + private static string GetCodecNameForAudioTag(ushort audioTag) + { + switch (audioTag) + { + case 1: { return "LPCM"; } + case 85: { return "MP3"; } + case 255: + case 5633: { return "AAC"; } + case 353: { return "WMA2"; } + case 354: { return "WMAP"; } + case 65534: { return "Vendor-extensible format"; } + default: { throw new Exception("Unsupported AudioTag '" + audioTag + "'!"); } + } + } + private class WaveFormatEx + { + public ushort wFormatTag; + public ushort nChannels; + public uint nSamplesPerSec; + public uint nAvgBytesPerSec; + public ushort nBlockAlign; + public ushort wBitsPerSample; + public byte[] DecoderSpecificData; // Max size is 65535 bytes. + public WaveFormatEx(byte[] data) + { + if (data == null || data.Length < 18) + { + throw new Exception("Invalid WaveFormatEx data!"); + } + ushort num = BitConverter.ToUInt16(data, 16); + if (data.Length != (int)(18 + num)) + { + throw new Exception("Invalid cbSize value!"); + } + this.wFormatTag = BitConverter.ToUInt16(data, 0); + this.nChannels = BitConverter.ToUInt16(data, 2); + this.nSamplesPerSec = (uint)BitConverter.ToUInt16(data, 4); + this.nAvgBytesPerSec = (uint)BitConverter.ToUInt16(data, 8); + this.nBlockAlign = BitConverter.ToUInt16(data, 12); + this.wBitsPerSample = BitConverter.ToUInt16(data, 14); + this.DecoderSpecificData = new byte[(int)num]; + Buffer.BlockCopy(data, 18, this.DecoderSpecificData, 0, this.DecoderSpecificData.Length); + } + public WaveFormatEx(ushort wFormatTag, ushort nChannels, uint nSamplesPerSec, uint nAvgBytesPerSec, ushort nBlockAlign, + ushort wBitsPerSample, byte[] DecoderSpecificData) + { + if (DecoderSpecificData != null && DecoderSpecificData.Length > 65535) + { + throw new Exception("DecoderSpecificData too long."); + } + this.wFormatTag = wFormatTag; + this.nChannels = nChannels; + this.nSamplesPerSec = nSamplesPerSec; + this.nAvgBytesPerSec = nAvgBytesPerSec; + this.nBlockAlign = nBlockAlign; + this.wBitsPerSample = wBitsPerSample; + this.DecoderSpecificData = DecoderSpecificData; + } + public byte[] GetBytes() + { + byte[] array = new byte[18 + this.DecoderSpecificData.Length]; + Buffer.BlockCopy(BitConverter.GetBytes(this.wFormatTag), 0, array, 0, 2); + Buffer.BlockCopy(BitConverter.GetBytes(this.nChannels), 0, array, 2, 2); + Buffer.BlockCopy(BitConverter.GetBytes(this.nSamplesPerSec), 0, array, 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(this.nAvgBytesPerSec), 0, array, 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(this.nBlockAlign), 0, array, 12, 2); + Buffer.BlockCopy(BitConverter.GetBytes(this.wBitsPerSample), 0, array, 14, 2); + Buffer.BlockCopy(BitConverter.GetBytes((ushort)this.DecoderSpecificData.Length), 0, array, 16, 2); + if (array.Length != 18) + { + Buffer.BlockCopy(this.DecoderSpecificData, 0, array, 18, this.DecoderSpecificData.Length); + } + return array; + } + } + private static readonly uint[] MP4_SamplingRate = new uint[] { + 96000u, 88200u, 64000u, 48000u, 44100u, 32000u, 24000u, 22050u, 16000u, 12000u, 11025u, 8000u, 7350u, 0u, 0u, 0u }; + private static readonly string MP4_Channels = "\x00\x01\x02\x03\x04\x05\x06\x08"; + private static byte[] GetAudioSpecificConfigBytes(uint samplingRate, byte numberOfChannels) + { + // public enum Profile : byte { MAIN = 1, LC, SSR, LTP, SBR,Scalable } + // ushort num = (ushort)((ushort)profile << 11); + ushort num = (ushort)((ushort)2 << 11); // Profile.LC. + int num2 = 0; + while (MP4_SamplingRate[num2] != samplingRate && num2 < MP4_SamplingRate.Length) + { + num2++; + } + if (num2 > MP4_SamplingRate.Length) + { + throw new Exception("Invalid sampling rate!"); + } + num += (ushort)((ushort)num2 << 7); + num2 = 0; + while (MP4_Channels[num2] != numberOfChannels && num2 < MP4_Channels.Length) + { + num2++; + } + if (num2 > MP4_Channels.Length) + { + throw new Exception("Invalid number of channels!"); + } + num += (ushort)((ushort)num2 << 3); + return Utils.InplaceReverseBytes(BitConverter.GetBytes(num)); + } + + public AudioTrackInfo(XmlNode element, IDictionary streamAttributes, uint index, StreamInfo stream) + : base(element, index, stream) + { + WaveFormatEx waveFormatEx; + if (base.Attributes.ContainsKey("WaveFormatEx")) + { + byte[] data = Parse.HexStringAttribute(base.Attributes, "WaveFormatEx"); + waveFormatEx = new WaveFormatEx(data); + } + else + { + ushort wFormatTag = Parse.UInt16Attribute(base.Attributes, "AudioTag"); + ushort nChannels = Parse.UInt16Attribute(base.Attributes, "Channels"); + uint nSamplesPerSec = Parse.UInt32Attribute(base.Attributes, "SamplingRate"); + uint num = Parse.UInt32Attribute(base.Attributes, "Bitrate"); + ushort nBlockAlign = Parse.UInt16Attribute(base.Attributes, "PacketSize"); + ushort wBitsPerSample = Parse.UInt16Attribute(base.Attributes, "BitsPerSample"); + byte[] decoderSpecificData = Parse.HexStringAttribute(base.Attributes, "CodecPrivateData"); + waveFormatEx = new WaveFormatEx(wFormatTag, nChannels, nSamplesPerSec, num / 8u, nBlockAlign, wBitsPerSample, decoderSpecificData); + } + byte[] audioInfoBytes = MkvUtils.GetAudioInfoBytes( + waveFormatEx.nSamplesPerSec, (ulong)waveFormatEx.nChannels, (ulong)waveFormatEx.wBitsPerSample); + switch (waveFormatEx.wFormatTag) + { + case 353: + case 354: + { + base.TrackEntry = new TrackEntry(TrackType.Audio, audioInfoBytes, CodecID.A_MS, waveFormatEx.GetBytes()); + break; + } + case 255: + case 5633: + { + base.TrackEntry = new TrackEntry(TrackType.Audio, audioInfoBytes, CodecID.A_AAC, GetAudioSpecificConfigBytes( + waveFormatEx.nSamplesPerSec, (byte)waveFormatEx.nChannels)); + break; + } + case 1: + { + throw new Exception("Unsupported audio format: 'LPCM'!"); + } + case 65534: + { + throw new Exception("Unsupported audio format: 'Vendor-extensible format'!"); + } + default: + { + throw new Exception("Unsupported AudioTag: '" + waveFormatEx.wFormatTag + "'"); + } + } + if (base.Attributes.ContainsKey("Name")) + { + base.TrackEntry.Name = Parse.StringAttribute(streamAttributes, "Name"); + } + base.TrackEntry.Language = LanguageID.Hungarian; // TODO: Make this configurable. + base.Description = string.Format("{0} {1} channels {2} Hz @ {3} kbps", new object[] { + GetCodecNameForAudioTag(waveFormatEx.wFormatTag), waveFormatEx.nChannels, waveFormatEx.nSamplesPerSec, + base.Bitrate / 1000u }); + } + } + + internal class VideoTrackInfo : TrackInfo + { + private static byte[] GetBitmapInfoHeaderBytes(int biWidth, int biHeight, ushort biPlanes, ushort biBitCount, + uint biCompression, uint biSizeImage, int biXPelsPerMeter, int biYPelsPerMeter, + uint biClrUsed, uint biClrImportant, byte[] codecPrivateData) + { + int biSize = 40 + codecPrivateData.Length; + byte[] array = new byte[biSize]; + Buffer.BlockCopy(BitConverter.GetBytes(biSize), 0, array, 0, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biWidth), 0, array, 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biHeight), 0, array, 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biPlanes), 0, array, 12, 2); + Buffer.BlockCopy(BitConverter.GetBytes(biBitCount), 0, array, 14, 2); + Buffer.BlockCopy(BitConverter.GetBytes(biCompression), 0, array, 16, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biSizeImage), 0, array, 20, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biXPelsPerMeter), 0, array, 24, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biYPelsPerMeter), 0, array, 28, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biClrUsed), 0, array, 32, 4); + Buffer.BlockCopy(BitConverter.GetBytes(biClrImportant), 0, array, 36, 4); + Buffer.BlockCopy(codecPrivateData, 0, array, 40, codecPrivateData.Length); + return array; + } + public VideoTrackInfo(XmlNode element, IDictionary streamAttributes, uint index, StreamInfo stream) + : base(element, index, stream) + { + uint pixelWidth = base.Attributes.ContainsKey("MaxWidth") ? Parse.UInt32Attribute(base.Attributes, "MaxWidth") : + base.Attributes.ContainsKey("Width") ? Parse.UInt32Attribute(base.Attributes, "Width") : + streamAttributes.ContainsKey("MaxWidth") ? Parse.UInt32Attribute(streamAttributes, "MaxWidth") : 0u; + if (pixelWidth == 0u) + { + throw new Exception("Missing video width attribute!"); + } + uint pixelHeight = base.Attributes.ContainsKey("MaxHeight") ? Parse.UInt32Attribute(base.Attributes, "MaxHeight") : + base.Attributes.ContainsKey("Height") ? Parse.UInt32Attribute(base.Attributes, "Height") : + streamAttributes.ContainsKey("MaxHeight") ? Parse.UInt32Attribute(streamAttributes, "MaxHeight") : 0u; + if (pixelHeight == 0u) + { + throw new Exception("Missing video height attribute!"); + } + uint displayWidth = streamAttributes.ContainsKey("DisplayWidth") ? + Parse.UInt32Attribute(streamAttributes, "DisplayWidth") : 0u; + if (displayWidth == 0u) + { + displayWidth = pixelWidth; + } + uint displayHeight = streamAttributes.ContainsKey("DisplayHeight") ? + Parse.UInt32Attribute(streamAttributes, "DisplayHeight") : 0u; + if (displayHeight == 0u) + { + displayHeight = pixelHeight; + } + byte[] videoInfoBytes = MkvUtils.GetVideoInfoBytes( + (ulong)pixelWidth, (ulong)pixelHeight, (ulong)displayWidth, (ulong)displayHeight); + byte[] codecPrivateData = base.Attributes.ContainsKey("CodecPrivateData") ? + Parse.HexStringAttribute(base.Attributes, "CodecPrivateData") : null; + if (codecPrivateData == null) + { + throw new Exception("Missing CodecPrivateData attribute!"); + } + string fourcc = base.Attributes.ContainsKey("FourCC") ? Parse.StringAttribute(base.Attributes, "FourCC") : + streamAttributes.ContainsKey("FourCC") ? Parse.StringAttribute(streamAttributes, "FourCC") : null; + switch (fourcc) + { + case "WVC1": + { + base.TrackEntry = new TrackEntry( + TrackType.Video, videoInfoBytes, CodecID.V_MS, VideoTrackInfo.GetVfWCodecPrivate( + pixelWidth, pixelHeight, fourcc, codecPrivateData)); + break; + } + case "H264": + { + ushort nalUnitLengthField = 4; + if (base.Attributes.ContainsKey("NALUnitLengthField")) + { + nalUnitLengthField = Parse.UInt16Attribute(base.Attributes, "NALUnitLengthField"); + } + base.TrackEntry = new TrackEntry( + TrackType.Video, videoInfoBytes, CodecID.V_AVC, + VideoTrackInfo.GetAVCCodecPrivate(codecPrivateData, nalUnitLengthField)); + break; + } + case null: + { + throw new Exception("Missing FourCC attribute!"); + } + default: + { + throw new Exception("Unsupported video FourCC: '" + fourcc + "'"); + } + } + if (base.Attributes.ContainsKey("Name")) + { + base.TrackEntry.Name = Parse.StringAttribute(streamAttributes, "Name"); + } + base.TrackEntry.Language = LanguageID.Hungarian; // TODO: Make this configurable. + base.Description = string.Format("{0} {1}x{2} ({3}x{4}) @ {5} kbps", new object[] { + fourcc, pixelWidth, pixelHeight, displayWidth, displayHeight, base.Bitrate / 1000u }); + } + private static byte[] GetAVCCodecPrivate(byte[] codecPrivateData, ushort nalUnitLengthField) + { + switch (nalUnitLengthField) + { + case 1: + case 2: + case 4: + { + string text = Utils.HexEncodeString(codecPrivateData); + if (string.IsNullOrEmpty(text)) + { + throw new Exception("Invalid AVC1 attribute: CodecPrivateData"); + } + string[] array = text.Split(new string[] { "00000001" }, 0); + if (array.Length != 3) + { + throw new Exception("Invalid AVC1 attribute: CodecPrivateData"); + } + byte[] array2 = Utils.HexDecodeString(array[1]); + if (array2 == null || array2.Length < 3) + { + throw new Exception("Invalid SPS in CodecPrivateData!"); + } + byte[] array3 = Utils.HexDecodeString(array[2]); + if (array3 == null) + { + throw new Exception("Invalid PPS in CodecPrivateData!"); + } + return GetAVCDecoderConfigurationBytes( + array2[1], array2[2], array2[3], (byte)(nalUnitLengthField - 1), + new byte[1][] { array2 }, new byte[1][] { array3 }); + } + } + throw new Exception("Invalid AVC1 attribute: NALUnitLengthField"); + } + private static byte[] GetAVCDecoderConfigurationBytes( + byte AVCProfileIndication, byte profile_compatibility, byte AVCLevelIndication, + byte lengthSizeMinusOne, byte[][] sequenceParameterSetNALUnits, byte[][] pictureParameterSetNALUnits) + { + if (lengthSizeMinusOne != 0 && lengthSizeMinusOne != 1 && lengthSizeMinusOne != 3) + { + throw new Exception("Invalid lengthSizeMinusOne value in AVCDecoderConfigurationRecord!"); + } + if (sequenceParameterSetNALUnits.Length > 31) + { + throw new Exception("Invalid sequenceParameterSetNALUnits count in AVCDecoderConfigurationRecord!"); + } + if (pictureParameterSetNALUnits.Length > 255) + { + throw new Exception("Invalid pictureParameterSetNALUnits count in AVCDecoderConfigurationRecord!"); + } + int i = 7; + int limitS = sequenceParameterSetNALUnits.Length; + for (int b = 0; b < limitS; ++b) + { + i += 2 + sequenceParameterSetNALUnits[b].Length; + } + int limitP = pictureParameterSetNALUnits.Length; + for (int b = 0; b < limitP; ++b) + { + i += 2 + pictureParameterSetNALUnits[b].Length; + } + byte[] array = new byte[i]; + array[0] = 1; // configurationVersion. + array[1] = AVCProfileIndication; + array[2] = profile_compatibility; + array[3] = AVCLevelIndication; + array[4] = (byte)(252 ^ lengthSizeMinusOne); + array[5] = (byte)(224 ^ limitS); + i = 6; + for (int b = 0; b < limitS; ++b) + { + int size = sequenceParameterSetNALUnits[b].Length; + array[i] = (byte)(size >> 8); + array[i + 1] = (byte)(size & 255); + i += 2; + Buffer.BlockCopy(sequenceParameterSetNALUnits[b], 0, array, i, size); + i += size; + } + array[i++] = (byte)limitP; + for (int b = 0; b < limitP; ++b) + { + int size = pictureParameterSetNALUnits[b].Length; + array[i] = (byte)(size >> 8); + array[i + 1] = (byte)(size & 255); + i += 2; + Buffer.BlockCopy(pictureParameterSetNALUnits[b], 0, array, i, size); + i += size; + } + return array; + } + private static byte[] GetVfWCodecPrivate(uint width, uint height, string fourCC, byte[] codecPrivateData) + { + if (width > 2147483647u) + { // int.MaxValue + throw new Exception("Invalid video width value!"); + } + if (height > 2147483647u) + { // int.MaxValue + throw new Exception("Invalid video height value!"); + } + if (fourCC.Length != 4) + { + throw new Exception("Invalid video FourCC value!"); + } + return GetBitmapInfoHeaderBytes( + (int)width, (int)height, 1, 24, + /*biCompression:*/BitConverter.ToUInt32(Encoding.ASCII.GetBytes(fourCC), 0), + width * height * 24u / 8u, 0, 0, 0u, 0u, codecPrivateData); + } + } + + internal class Parse + { + public static IDictionary Attributes(XmlNode element) + { + Dictionary dictionary = new Dictionary(); + foreach (XmlAttribute xmlAttribute in element.Attributes) + { + dictionary.Add(xmlAttribute.Name, xmlAttribute.Value); + } + return dictionary; + } + public static IDictionary CustomAttributes(XmlNode element) + { + Dictionary dictionary = new Dictionary(); + XmlNode xmlNode = element.SelectSingleNode("CustomAttributes"); + if (xmlNode != null) + { + foreach (XmlNode xmlNode2 in xmlNode.SelectNodes("Attribute")) + { + dictionary.Add(xmlNode2.Attributes.GetNamedItem("Name").Value, xmlNode2.Attributes.GetNamedItem("Value").Value); + } + } + return dictionary; + } + public static string StringAttribute(IDictionary attributes, string key) + { + if (!attributes.ContainsKey(key)) + { + throw new Exception(key + " key is missing!"); + } + return attributes[key]; + } + public static bool BoolAttribute(IDictionary attributes, string key) + { + string text = Parse.StringAttribute(attributes, key); + bool result; + if (!bool.TryParse(text, out result)) + { + throw new Exception("Cannot parse the " + key + " key!"); + } + return result; + } + public static ushort UInt16Attribute(IDictionary attributes, string key) + { + string text = Parse.StringAttribute(attributes, key); + ushort result; + if (!ushort.TryParse(text, out result)) + { + throw new Exception("Cannot parse the " + key + " key!"); + } + return result; + } + public static uint UInt32Attribute(IDictionary attributes, string key) + { + string text = Parse.StringAttribute(attributes, key); + uint result; + if (!uint.TryParse(text, out result)) + { + throw new Exception("Cannot parse the " + key + " key!"); + } + return result; + } + public static ulong UInt64Attribute(IDictionary attributes, string key) + { + string text = Parse.StringAttribute(attributes, key); + ulong result; + if (!ulong.TryParse(text, out result)) + { + throw new Exception("Cannot parse the " + key + " key!"); + } + return result; + } + public static byte[] HexStringAttribute(IDictionary attributes, string key) + { + return Utils.HexDecodeString(Parse.StringAttribute(attributes, key)); + } + public static MediaStreamType MediaStreamTypeAttribute(IDictionary attributes, string key) + { + switch (Parse.StringAttribute(attributes, key)) + { + case "video": { return MediaStreamType.Video; } + case "audio": { return MediaStreamType.Audio; } + case "text": { return MediaStreamType.Script; } + default: { throw new Exception("Cannot parse the " + key + " key!"); } + } + } + } +} diff --git a/src/Utils.cs b/src/Utils.cs new file mode 100644 index 0000000..a3329dc --- /dev/null +++ b/src/Utils.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +namespace Smoothget +{ + public class Utils + { + public static byte[] InplaceReverseBytes(byte[] a) + { + Array.Reverse(a); + return a; + } + public static byte[] CombineByteArrays(IList arrays) + { + int totalSize = 0; + for (int i = 0; i < arrays.Count; ++i) + { + totalSize += arrays[i].Length; + } + byte[] array = new byte[totalSize]; + int pos = 0; + for (int i = 0; i < arrays.Count; ++i) + { + Buffer.BlockCopy(arrays[i], 0, array, pos, arrays[i].Length); + pos += arrays[i].Length; + } + return array; + } + public static byte[] CombineByteArraysAndArraySegments(IList arrays, IList> arraySegments) + { + int totalSize = 0; + if (arrays != null) + { + for (int i = 0; i < arrays.Count; ++i) + { + totalSize += arrays[i].Length; + } + } + for (int i = 0; i < arraySegments.Count; ++i) + { + totalSize += arraySegments[i].Count; + } + byte[] array = new byte[totalSize]; + int pos = 0; + if (arrays != null) + { + for (int i = 0; i < arrays.Count; ++i) + { + Buffer.BlockCopy(arrays[i], 0, array, pos, arrays[i].Length); + pos += arrays[i].Length; + } + } + for (int i = 0; i < arraySegments.Count; ++i) + { + Buffer.BlockCopy(arraySegments[i].Array, arraySegments[i].Offset, array, pos, arraySegments[i].Count); + pos += arraySegments[i].Count; + } + return array; + } + public static byte[] CombineBytes(byte[] a, byte[] b) + { + byte[] array = new byte[a.Length + b.Length]; + Buffer.BlockCopy(a, 0, array, 0, a.Length); + Buffer.BlockCopy(b, 0, array, a.Length, b.Length); + return array; + } + public static byte[] CombineBytes(byte[] a, byte[] b, byte[] c) + { + byte[] array = new byte[a.Length + b.Length + c.Length]; + Buffer.BlockCopy(a, 0, array, 0, a.Length); + Buffer.BlockCopy(b, 0, array, a.Length, b.Length); + Buffer.BlockCopy(c, 0, array, a.Length + b.Length, c.Length); + return array; + } + public static string HexEncodeString(byte[] bytes) + { + // This is simple and fast, but the complicated implementation below is faster, + // http://stackoverflow.com/questions/623104/c-sharp-byte-to-hex-string + // return BitConverter.ToString(input).Replace("-", ""); + char[] c = new char[bytes.Length << 1]; + byte b; + for (int bx = 0, cx = 0; bx < bytes.Length; ++bx) + { + b = ((byte)(bytes[bx] >> 4)); + c[cx++] = (char)(b > 9 ? b + 0x57 : b + 0x30); + b = ((byte)(bytes[bx] & 0x0F)); + c[cx++] = (char)(b > 9 ? b + 0x57 : b + 0x30); + } + return new string(c); + } + public static byte[] HexDecodeString(string hexEncodedData) + { + // This is a fast implementation from http://stackoverflow.com/questions/623104/c-sharp-byte-to-hex-string + if (hexEncodedData == null) return null; + byte[] buffer = new byte[hexEncodedData.Length >> 1]; + char c; + for (int bx = 0, sx = 0; bx < buffer.Length; ++bx) + { + c = hexEncodedData[sx++]; + if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; + buffer[bx] = (byte)((c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')) << 4); + c = hexEncodedData[sx++]; + if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; + buffer[bx] |= (byte)(c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')); + } + return buffer; + } + // TODO: Save code size by using Encoding.ASCII and HexEncodeString. + public static byte[] HexDecodeBytes(byte[] hexEncodedData, int start, int end) + { + // This is a fast implementation based on http://stackoverflow.com/questions/623104/c-sharp-byte-to-hex-string + if (hexEncodedData == null) return null; + if (start < 0) start = 0; + if (end >= hexEncodedData.Length) end = hexEncodedData.Length; + if (start >= end) return new byte[] { }; + if (((end - start) & 1) != 0) return null; + byte[] buffer = new byte[(end - start) >> 1]; + byte c; + for (int bx = 0, sx = start; bx < buffer.Length; ++bx) + { + c = hexEncodedData[sx++]; + if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; + buffer[bx] = (byte)((c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')) << 4); + c = hexEncodedData[sx++]; + if (((uint)c - (uint)'0') > 9 && ((uint)c - (uint)'A') > 5 && ((uint)c - (uint)'a') > 5) return null; + buffer[bx] |= (byte)(c > '9' ? (c > 'Z' ? (c - 'a' + 10) : (c - 'A' + 10)) : (c - '0')); + } + return buffer; + } + public static string EscapeString(string s) + { + // This is simpler and less bloated than the CSharpCodeProvider in + // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal + // TODO: Save memory by doing less concatenations. + return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } + // Always makes a copy. + public static byte[] GetSubBytes(byte[] bytes, int start, int end) + { + if (bytes == null) return null; + if (start < 0) start = 0; + if (end >= bytes.Length) end = bytes.Length; + if (start >= end) return new byte[] { }; + byte[] subBytes = new byte[end - start]; + Buffer.BlockCopy(bytes, start, subBytes, 0, end - start); + return subBytes; + } + public static bool ArePrefixBytesEqual(byte[] a, byte[] b, int size) + { + for (int i = 0; i < size; ++i) + { + if (a[i] != b[i]) return false; + } + return true; + } + } +} From 32635fcc606178975656c4128dd7dfee45482d8e Mon Sep 17 00:00:00 2001 From: Milek7 Date: Fri, 20 Jun 2014 14:03:39 +0200 Subject: [PATCH 2/5] added -vq and -aq flags to select quality --- src/Program.cs | 19 +++++++++++++++++++ src/Smooth.cs | 23 +++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Program.cs b/src/Program.cs index 9fddac3..2bb71ed 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -40,6 +40,9 @@ public void Process(IList inputUrls, IList outputUrls) internal class MainClass { + public static int audioQuality = 0; //TODO: Move audioQuality and videoQuality somewhere else. + public static int videoQuality = 0; + private static void Main(string[] args) { Logo(); @@ -60,6 +63,22 @@ private static void Main(string[] args) { isDeterministic = true; } + else if (args[j] == "--vq") + { + ++j; + if (j <= args.Length) + { + videoQuality = Convert.ToInt32(args[j]); + } + } + else if (args[j] == "--aq") + { + ++j; + if (j <= args.Length) + { + audioQuality = Convert.ToInt32(args[j]); + } + } } if (args.Length < j + 2) { diff --git a/src/Smooth.cs b/src/Smooth.cs index d82b2e0..ec1df32 100644 --- a/src/Smooth.cs +++ b/src/Smooth.cs @@ -200,9 +200,28 @@ public StreamInfo(XmlNode element, Uri manifestUri) this.pureUrl = this.pureUrl.Substring(0, this.pureUrl.LastIndexOf('/')); this.SelectedTracks = new List(); - if (this.AvailableTracks.Count > 0) + if (this.AvailableTracks[0] is AudioTrackInfo) { - this.SelectedTracks.Add(this.AvailableTracks[0]); + if (this.AvailableTracks.Count >= MainClass.audioQuality) + { + this.SelectedTracks.Add(this.AvailableTracks[MainClass.audioQuality]); + } + else + { + this.SelectedTracks.Add(this.AvailableTracks[0]); + } + } + + if (this.AvailableTracks[0] is VideoTrackInfo) + { + if (this.AvailableTracks.Count >= MainClass.videoQuality) + { + this.SelectedTracks.Add(this.AvailableTracks[MainClass.videoQuality]); + } + else + { + this.SelectedTracks.Add(this.AvailableTracks[0]); + } } } private void CheckUrlAttribute() From f315d26e63f88b8d264a312ebe82970a060cfa10 Mon Sep 17 00:00:00 2001 From: Milek7 Date: Fri, 20 Jun 2014 19:24:44 +0200 Subject: [PATCH 3/5] updated version number --- compile-visualstudio/Properties/AssemblyInfo.cs | 4 ++-- src/Program.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/compile-visualstudio/Properties/AssemblyInfo.cs b/compile-visualstudio/Properties/AssemblyInfo.cs index 02de1f8..e6770de 100644 --- a/compile-visualstudio/Properties/AssemblyInfo.cs +++ b/compile-visualstudio/Properties/AssemblyInfo.cs @@ -6,11 +6,11 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("smoothget")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("ISS Smooth Streaming downloader")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("smoothget")] -[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyCopyright("")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/Program.cs b/src/Program.cs index 2bb71ed..f856474 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; using System.Threading; -//[assembly: AssemblyVersion("3.0.0.3")] namespace Smoothget { @@ -253,7 +252,7 @@ private static void RecordAndMux(string ismFileName, string outputDirectory, boo private static void Logo() { AssemblyName name = Assembly.GetEntryAssembly().GetName(); - Console.WriteLine(string.Concat(new object[] { name.Name, " v", name.Version })); + Console.WriteLine(string.Concat(new object[] { name.Name, " v1.0-alpha1"})); Console.WriteLine(); } private static void Help() From 672815a2c4e5057519ec3514eb6ec148755197b3 Mon Sep 17 00:00:00 2001 From: Milek7 Date: Fri, 20 Jun 2014 20:18:40 +0200 Subject: [PATCH 4/5] catching all exceptions to prevent application crash --- compile-visualstudio/smoothget.csproj | 3 +- src/Program.cs | 98 +++++++++++++++------------ 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/compile-visualstudio/smoothget.csproj b/compile-visualstudio/smoothget.csproj index 7eec541..807db0a 100644 --- a/compile-visualstudio/smoothget.csproj +++ b/compile-visualstudio/smoothget.csproj @@ -27,7 +27,8 @@ pdbonly true bin\Release\ - TRACE + + prompt 4 diff --git a/src/Program.cs b/src/Program.cs index f856474..5d4b417 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -44,65 +44,73 @@ internal class MainClass private static void Main(string[] args) { - Logo(); - int j; - bool isDeterministic = false; - for (j = 0; j < args.Length; ++j) + try { - if (args[j] == "--") + Logo(); + int j; + bool isDeterministic = false; + for (j = 0; j < args.Length; ++j) { - ++j; - break; + if (args[j] == "--") + { + ++j; + break; + } + else if (args[j].Length == 0 || args[j] == "-" || args[j][0] != '-') + { + break; + } + else if (args[j] == "--det") + { + isDeterministic = true; + } + else if (args[j] == "--vq") + { + ++j; + if (j <= args.Length) + { + videoQuality = Convert.ToInt32(args[j]); + } + } + else if (args[j] == "--aq") + { + ++j; + if (j <= args.Length) + { + audioQuality = Convert.ToInt32(args[j]); + } + } } - else if (args[j].Length == 0 || args[j] == "-" || args[j][0] != '-') + if (args.Length < j + 2) { - break; + Help(); } - else if (args[j] == "--det") + int lastIdx = args.Length - 1; + string downloadDirectory = args[lastIdx].Trim(Path.GetInvalidFileNameChars()).Trim(Path.GetInvalidPathChars()); + Console.WriteLine("Download directory: " + downloadDirectory); + string[] urls = new string[lastIdx - j]; + for (int i = j; i < lastIdx; ++i) { - isDeterministic = true; + urls[i - j] = args[i]; } - else if (args[j] == "--vq") + IList partUrls = ProcessUrls(urls); + Console.WriteLine("Parts to download:"); + for (int i = 0; i < partUrls.Count; i++) { - ++j; - if (j <= args.Length) - { - videoQuality = Convert.ToInt32(args[j]); - } + Console.WriteLine(" Part URL: " + partUrls[i]); } - else if (args[j] == "--aq") + Console.WriteLine(); + for (int i = 0; i < partUrls.Count; i++) { - ++j; - if (j <= args.Length) - { - audioQuality = Convert.ToInt32(args[j]); - } + RecordAndMux(partUrls[i], downloadDirectory, isDeterministic); } + Console.WriteLine("All downloading and muxing done."); } - if (args.Length < j + 2) - { - Help(); - } - int lastIdx = args.Length - 1; - string downloadDirectory = args[lastIdx].Trim(Path.GetInvalidFileNameChars()).Trim(Path.GetInvalidPathChars()); - Console.WriteLine("Download directory: " + downloadDirectory); - string[] urls = new string[lastIdx - j]; - for (int i = j; i < lastIdx; ++i) - { - urls[i - j] = args[i]; - } - IList partUrls = ProcessUrls(urls); - Console.WriteLine("Parts to download:"); - for (int i = 0; i < partUrls.Count; i++) - { - Console.WriteLine(" Part URL: " + partUrls[i]); - } - Console.WriteLine(); - for (int i = 0; i < partUrls.Count; i++) + catch (Exception e) { - RecordAndMux(partUrls[i], downloadDirectory, isDeterministic); + Console.WriteLine("Exception: {0}", e.Message); + Environment.Exit(-1); } - Console.WriteLine("All downloading and muxing done."); } // May return the same reference (urls). From 99e226bf219458bdf40b3a28ef0b61fb6a549649 Mon Sep 17 00:00:00 2001 From: Milek7 Date: Wed, 12 Nov 2014 20:10:35 +0100 Subject: [PATCH 5/5] fix absolute paths on unix systems --- src/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Program.cs b/src/Program.cs index 5d4b417..57a93d2 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -86,7 +86,7 @@ private static void Main(string[] args) Help(); } int lastIdx = args.Length - 1; - string downloadDirectory = args[lastIdx].Trim(Path.GetInvalidFileNameChars()).Trim(Path.GetInvalidPathChars()); + string downloadDirectory = args[lastIdx].Trim(Path.GetInvalidPathChars()); Console.WriteLine("Download directory: " + downloadDirectory); string[] urls = new string[lastIdx - j]; for (int i = j; i < lastIdx; ++i)