public AudioTrackInfo(XmlNode element, IDictionary <string, string> 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 }); }
/// <inheritdoc /> /// <summary> /// </summary> /// <param name="element"></param> /// <param name="streamAttributes"></param> /// <param name="position"></param> /// <param name="stream"></param> public AudioTrackInfo(XmlNode element, IDictionary <string, string> streamAttributes, uint position, StreamInfo stream) : base(element, position, stream) { WaveFormatEx waveFormatEx; if (Attributes.ContainsKey("WaveFormatEx")) { waveFormatEx = new WaveFormatEx(Parser.HexStringAttribute(Attributes, "WaveFormatEx")); } else { waveFormatEx = new WaveFormatEx( Parser.UInt16Attribute(Attributes, "AudioTag"), Parser.UInt16Attribute(Attributes, "Channels"), Parser.UInt32Attribute(Attributes, "SamplingRate"), Parser.UInt32Attribute(Attributes, "Bitrate") / 8, Parser.UInt16Attribute(Attributes, "PacketSize"), Parser.UInt16Attribute(Attributes, "BitsPerSample"), Parser.HexStringAttribute(Attributes, "CodecPrivateData")); } var audioInfoBytes = MkvUtils.GetAudioInfoBytes(waveFormatEx.NSamplesPerSec, waveFormatEx.NChannels, waveFormatEx.WBitsPerSample); switch (waveFormatEx.WFormatTag) { case 0x0161: case 0x0162: TrackEntry = new TrackEntry( MkvTrackType.Audio, audioInfoBytes, MkvCodec.AudioMs, waveFormatEx.GetBytes()); break; case 0x00FF: case 0x1601: TrackEntry = new TrackEntry( MkvTrackType.Audio, audioInfoBytes, MkvCodec.AudioAac, GetAudioSpecificConfigBytes(waveFormatEx.NSamplesPerSec, (byte)waveFormatEx.NChannels)); break; default: throw new Exception(); } if (Attributes.ContainsKey("Name")) { TrackEntry.Name = Parser.StringAttribute(streamAttributes, "Name"); } TrackEntry.Language = LanguageId.Hungarian; Description = $"{GetCodecNameForAudioTag(waveFormatEx.WFormatTag)} {waveFormatEx.NChannels} channels {waveFormatEx.NSamplesPerSec} Hz @ {Bitrate / 1000u} kbps"; }
/// <summary> /// </summary> /// <returns></returns> public byte[] GetBytes() { if (TrackNumber == 0) { throw new Exception(); } var contents = new List <byte[]> { MkvUtils.GetEeBytes(MkvIdentifier.TrackNumber, MkvUtils.GetVintBytes(TrackNumber)), MkvUtils.GetEeBytes(MkvIdentifier.TrackUid, MkvUtils.GetVintBytes(TrackNumber)), MkvUtils.GetEeBytes(MkvIdentifier.TrackType, MkvUtils.GetVintBytes((ulong)TrackType)), MkvUtils.GetEeBytes(MkvIdentifier.FlagEnabled, MkvUtils.GetVIntForFlag(true)), MkvUtils.GetEeBytes(MkvIdentifier.FlagDefault, MkvUtils.GetVIntForFlag(true)), MkvUtils.GetEeBytes(MkvIdentifier.FlagForced, MkvUtils.GetVIntForFlag(false)), MkvUtils.GetEeBytes(MkvIdentifier.FlagLacing, MkvUtils.GetVIntForFlag(true)) }; if (string.IsNullOrEmpty(Name) == false) { contents.Add(MkvUtils.GetEeBytes(MkvIdentifier.Name, Encoding.UTF8.GetBytes(Name))); } if (Language != LanguageId.English) { contents.Add(MkvUtils.GetEeBytes(MkvIdentifier.Language, Encoding.ASCII.GetBytes(GetLanguageCode(Language)))); } contents.Add(MkvUtils.GetEeBytes(MkvIdentifier.CodecId, Encoding.ASCII.GetBytes(MkvUtils.GetStringForCodecId(MkvCodec)))); if (_codecPrivate != null) { contents.Add(MkvUtils.GetEeBytes(MkvIdentifier.CodecPrivate, _codecPrivate)); } contents.Add(_infoBytes); return(MkvUtils.GetEeBytes(MkvIdentifier.TrackEntry, Utils.CombineByteArrays(contents))); }
/// <inheritdoc /> /// <summary> /// </summary> /// <param name="element"></param> /// <param name="attributes"></param> /// <param name="position"></param> /// <param name="stream"></param> public VideoTrackInfo(XmlNode element, IDictionary <string, string> attributes, uint position, StreamInfo stream) : base(element, position, stream) { var pixelWidth = 0u; var pixelHeight = 0u; var displayWidth = 0u; var displayHeight = 0u; if (Attributes.ContainsKey("MaxWidth")) { pixelWidth = Parser.UInt32Attribute(Attributes, "MaxWidth"); } else if (Attributes.ContainsKey("Width")) { pixelWidth = Parser.UInt32Attribute(Attributes, "Width"); } else if (attributes.ContainsKey("MaxWidth")) { pixelWidth = Parser.UInt32Attribute(attributes, "MaxWidth"); } if (pixelWidth == 0u) { throw new Exception(); } if (Attributes.ContainsKey("MaxHeight")) { pixelHeight = Parser.UInt32Attribute(Attributes, "MaxHeight"); } else if (Attributes.ContainsKey("Height")) { pixelHeight = Parser.UInt32Attribute(Attributes, "Height"); } else if (attributes.ContainsKey("MaxHeight")) { pixelHeight = Parser.UInt32Attribute(attributes, "MaxHeight"); } if (pixelHeight == 0u) { throw new Exception(); } if (attributes.ContainsKey("DisplayWidth")) { displayWidth = Parser.UInt32Attribute(attributes, "DisplayWidth"); } if (displayWidth == 0u) { displayWidth = pixelWidth; } if (attributes.ContainsKey("DisplayHeight")) { displayHeight = Parser.UInt32Attribute(attributes, "DisplayHeight"); } if (displayHeight == 0u) { displayHeight = pixelHeight; } var videoInfoBytes = MkvUtils.GetVideoInfoBytes(pixelWidth, pixelHeight, displayWidth, displayHeight); byte[] codecPrivateData = null; if (Attributes.ContainsKey("CodecPrivateData")) { codecPrivateData = Parser.HexStringAttribute(Attributes, "CodecPrivateData"); } if (codecPrivateData == null) { throw new Exception(); } string fourcc = null; if (Attributes.ContainsKey("FourCC")) { fourcc = Parser.StringAttribute(Attributes, "FourCC"); } else if (attributes.ContainsKey("FourCC")) { fourcc = Parser.StringAttribute(attributes, "FourCC"); } switch (fourcc) { case "WVC1": TrackEntry = new TrackEntry( MkvTrackType.Video, videoInfoBytes, MkvCodec.VideoMs, GetVfWCodecPrivate( pixelWidth, pixelHeight, fourcc, codecPrivateData)); break; case "H264": ushort nalUnitLengthField = 4; if (Attributes.ContainsKey("NALUnitLengthField")) { nalUnitLengthField = Parser.UInt16Attribute(Attributes, "NALUnitLengthField"); } TrackEntry = new TrackEntry(MkvTrackType.Video, videoInfoBytes, MkvCodec.VideoAvc, GetAvcCodecPrivate(codecPrivateData, nalUnitLengthField)); break; case null: throw new Exception(); default: throw new Exception(); } if (Attributes.ContainsKey("Name")) { TrackEntry.Name = Parser.StringAttribute(attributes, "Name"); } TrackEntry.Language = LanguageId.Hungarian; Description = $"{fourcc} {pixelWidth}x{pixelHeight} ({displayWidth}x{displayHeight}) @ {Bitrate / 1000u} kbps"; }
public VideoTrackInfo(XmlNode element, IDictionary <string, string> 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 }); }
// 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 <Track> tracks = new List <Track>(); foreach (StreamInfo streamInfo in manifestInfo.SelectedStreams) { foreach (TrackInfo trackInfo in streamInfo.SelectedTracks) { tracks.Add(new Track(trackInfo)); } } IList <TrackEntry> trackEntries = new List <TrackEntry>(); IList <IList <MediaSample> > trackSamples = new List <IList <MediaSample> >(); for (int i = 0; i < tracks.Count; ++i) { trackEntries.Add(tracks[i].TrackInfo.TrackEntry); trackEntries[i].TrackNumber = (ulong)(i + 1); trackSamples.Add(new List <MediaSample>()); } 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 <ChunkInfo> 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); } }
/// <summary> /// </summary> /// <param name="manifestUri"></param> /// <param name="manifestPath"></param> /// <param name="mkvPath"></param> /// <param name="isDeterministic"></param> /// <param name="stopAfter"></param> /// <param name="setupStop"></param> /// <param name="displayDuration"></param> public static void DownloadAndMux(Uri manifestUri, string manifestPath, string mkvPath, bool isDeterministic, TimeSpan stopAfter, SetupStop setupStop, DisplayDuration displayDuration) { string manifestParentPath = null; ManifestInfo manifestInfo; if (manifestPath != null) { manifestParentPath = Path.GetDirectoryName(manifestPath); Console.WriteLine($"Parsing local manifest file: {manifestPath}"); using (var manifestStream = new FileStream(manifestPath, FileMode.Open)) { manifestInfo = ManifestInfo.ParseManifest(manifestStream, new Uri(LocalUrlPrefix)); } } else { Console.WriteLine($"Downloading and parsing manifest: {manifestUri}"); using (var manifestStream = new WebClient().OpenRead(manifestUri)) { manifestInfo = ManifestInfo.ParseManifest(manifestStream, manifestUri); } } Console.Write(manifestInfo.GetDescription()); var tracks = ( from streamInfo in manifestInfo.SelectedStreams from trackInfo in streamInfo.SelectedTracks select new Track(trackInfo) ).ToList(); var trackEntries = new List <TrackEntry>(); var trackSamples = new List <IList <MediaSample> >(); for (var i = 0; i < tracks.Count; ++i) { trackEntries.Add(tracks[i].TrackInfo.TrackEntry); trackEntries[i].TrackNumber = (ulong)(i + 1); trackSamples.Add(new List <MediaSample>()); } foreach (var track in tracks) { if (track.TrackInfo.Stream.ChunkList.Count == 0) { track.NextStartTime = 0; } else { track.NextStartTime = track.TrackInfo.Stream.ChunkList[0].StartTime; } } 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) { throw new Exception(); } var maxTrackEndTimeHint = manifestInfo.Duration; foreach (var track in tracks) { var chunkInfos = track.TrackInfo.Stream.ChunkList; var lastIndex = chunkInfos.Count - 1; if (lastIndex < 0) { continue; } var trackDuration = chunkInfos[lastIndex].StartTime + chunkInfos[lastIndex].Duration; if (maxTrackEndTimeHint < trackDuration) { maxTrackEndTimeHint = trackDuration; } } var muxStatePath = Path.ChangeExtension(mkvPath, "muxstate"); var muxStateOldPath = muxStatePath + ".old"; byte[] previousMuxState = null; if (File.Exists(muxStatePath)) { using (var stream = new FileStream(muxStatePath ?? throw new Exception(), FileMode.Open)) { previousMuxState = ReadFileStream(stream); } if (previousMuxState.Length > 0) { try { File.Move(muxStatePath, muxStateOldPath); } catch (IOException) { File.Replace(muxStatePath, muxStateOldPath, null, true); } } } var source = new DownloadingMediaDataSource(tracks, manifestParentPath, manifestInfo.TimeScale, manifestInfo.IsLive, (ulong)stopAfter.Ticks, manifestInfo.TotalTicks, displayDuration); setupStop(manifestInfo.IsLive, source); var muxStateWriter = new MuxStateWriter(new FileStream(muxStatePath, FileMode.Create)); try { MkvUtils.WriteMkv(mkvPath, trackEntries, source, maxTrackEndTimeHint, manifestInfo.TimeScale, isDeterministic, previousMuxState, muxStateWriter); } finally { muxStateWriter.Close(); } File.Delete(muxStatePath); if (File.Exists(muxStateOldPath)) { File.Delete(muxStateOldPath); } }
/// <summary> /// </summary> /// <returns></returns> public byte[] GetBytes() => MkvUtils.GetEeBytes(MkvIdentifier.CuePoint, Utils.CombineBytes( MkvUtils.GetEeBytes(MkvIdentifier.CueTime, MkvUtils.GetVintBytes(_cueTime)), MkvUtils.GetEeBytes(MkvIdentifier.CueTrackPositions, Utils.CombineBytes( MkvUtils.GetEeBytes(MkvIdentifier.CueTrack, MkvUtils.GetVintBytes(_cueTrack)), MkvUtils.GetEeBytes(MkvIdentifier.CueClusterPosition, MkvUtils.GetVintBytes(CueClusterPosition))))) );