using System; using System.Collections.Generic; using System.Drawing; using System.IO; namespace Nikse.SubtitleEdit.Core.VobSub { public class VobSubParser { public bool IsPal { get; private set; } public List VobSubPacks { get; private set; } public List IdxPalette = new List(); public List IdxLanguages = new List(); private const int PacketizedElementaryStreamMaximumLength = 2028; public VobSubParser(bool isPal) { IsPal = isPal; VobSubPacks = new List(); } public void Open(string fileName) { using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { Open(fs); } } /// /// Can be used with e.g. MemoryStream or FileStream /// /// public void Open(Stream ms) { VobSubPacks = new List(); ms.Position = 0; var buffer = new byte[0x800]; // 2048 long position = 0; while (position < ms.Length) { ms.Seek(position, SeekOrigin.Begin); ms.Read(buffer, 0, 0x0800); if (IsSubtitlePack(buffer)) VobSubPacks.Add(new VobSubPack(buffer, null)); position += 0x800; } } public void OpenSubIdx(string vobSubFileName, string idxFileName) { VobSubPacks = new List(); if (!string.IsNullOrEmpty(idxFileName) && File.Exists(idxFileName)) { var idx = new Idx(idxFileName); IdxPalette = idx.Palette; IdxLanguages = idx.Languages; if (idx.IdxParagraphs.Count > 0) { var buffer = new byte[0x800]; // 2048 using (var fs = new FileStream(vobSubFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { foreach (var p in idx.IdxParagraphs) { if (p.FilePosition + 100 < fs.Length) { long position = p.FilePosition; fs.Seek(position, SeekOrigin.Begin); fs.Read(buffer, 0, 0x0800); if (IsSubtitlePack(buffer) || IsPrivateStream1(buffer, 0)) { var vsp = new VobSubPack(buffer, p); VobSubPacks.Add(vsp); if (IsPrivateStream1(buffer, 0)) position += vsp.PacketizedElementaryStream.Length + 6; else position += 0x800; int currentSubPictureStreamId = vsp.PacketizedElementaryStream.SubPictureStreamId.Value; while (vsp.PacketizedElementaryStream != null && vsp.PacketizedElementaryStream.SubPictureStreamId.HasValue && (vsp.PacketizedElementaryStream.Length == PacketizedElementaryStreamMaximumLength || currentSubPictureStreamId != vsp.PacketizedElementaryStream.SubPictureStreamId.Value) && position < fs.Length) { fs.Seek(position, SeekOrigin.Begin); fs.Read(buffer, 0, 0x800); vsp = new VobSubPack(buffer, p); // idx position? if (vsp.PacketizedElementaryStream != null && vsp.PacketizedElementaryStream.SubPictureStreamId.HasValue && currentSubPictureStreamId == vsp.PacketizedElementaryStream.SubPictureStreamId.Value) { VobSubPacks.Add(vsp); if (IsPrivateStream1(buffer, 0)) position += vsp.PacketizedElementaryStream.Length + 6; else position += 0x800; } else { position += 0x800; fs.Seek(position, SeekOrigin.Begin); } } } } } } return; } } // No valid idx file found - just open like vob file Open(vobSubFileName); } /// /// Demultiplex multiplexed packs together each streamId at a time + removing bad packs + fixing displaytimes /// /// List of complete packs each with a complete sub image public List MergeVobSubPacks() { var list = new List(); var pts = new TimeSpan(); var ms = new MemoryStream(); int streamId = 0; float ticksPerMillisecond = 90.000F; if (!IsPal) ticksPerMillisecond = 90.090F; // TODO: What should this be for NTSC? // get unique streamIds var uniqueStreamIds = new List(); foreach (var p in VobSubPacks) { if (p.PacketizedElementaryStream != null && p.PacketizedElementaryStream.SubPictureStreamId.HasValue && !uniqueStreamIds.Contains(p.PacketizedElementaryStream.SubPictureStreamId.Value)) uniqueStreamIds.Add(p.PacketizedElementaryStream.SubPictureStreamId.Value); } IdxParagraph lastIdxParagraph = null; foreach (int uniqueStreamId in uniqueStreamIds) // packets must be merged in streamId order (so they don't get mixed) { foreach (var p in VobSubPacks) { if (p.PacketizedElementaryStream != null && p.PacketizedElementaryStream.SubPictureStreamId.HasValue && p.PacketizedElementaryStream.SubPictureStreamId.Value == uniqueStreamId) { if (p.PacketizedElementaryStream.PresentationTimestampDecodeTimestampFlags > 0) { if (lastIdxParagraph == null || p.IdxLine.FilePosition != lastIdxParagraph.FilePosition) { if (ms.Length > 0) list.Add(new VobSubMergedPack(ms.ToArray(), pts, streamId, lastIdxParagraph)); ms.Close(); ms = new MemoryStream(); pts = TimeSpan.FromMilliseconds(Convert.ToDouble(p.PacketizedElementaryStream.PresentationTimestamp / ticksPerMillisecond)); //90000F * 1000)); (PAL) streamId = p.PacketizedElementaryStream.SubPictureStreamId.Value; } } lastIdxParagraph = p.IdxLine; p.PacketizedElementaryStream.WriteToStream(ms); } } if (ms.Length > 0) { list.Add(new VobSubMergedPack(ms.ToArray(), pts, streamId, lastIdxParagraph)); ms.Close(); ms = new MemoryStream(); } } ms.Close(); // Remove any bad packs for (int i = list.Count - 1; i >= 0; i--) { VobSubMergedPack pack = list[i]; if (pack.SubPicture == null || pack.SubPicture.ImageDisplayArea.Width <= 3 || pack.SubPicture.ImageDisplayArea.Height <= 2) list.RemoveAt(i); else if (pack.EndTime.TotalSeconds - pack.StartTime.TotalSeconds < 0.1 && pack.SubPicture.ImageDisplayArea.Width <= 10 && pack.SubPicture.ImageDisplayArea.Height <= 10) list.RemoveAt(i); } // Fix subs with no duration (completely normal) or negative duration or duration > 10 seconds for (int i = 0; i < list.Count; i++) { VobSubMergedPack pack = list[i]; if (pack.SubPicture.Delay.TotalMilliseconds > 0) pack.EndTime = pack.StartTime.Add(pack.SubPicture.Delay); if (pack.EndTime < pack.StartTime || pack.EndTime.TotalSeconds - pack.StartTime.TotalSeconds > 10.0) { if (i + 1 < list.Count) pack.EndTime = TimeSpan.FromMilliseconds(list[i].StartTime.TotalMilliseconds - 100); else pack.EndTime = TimeSpan.FromMilliseconds(pack.StartTime.TotalMilliseconds + 3000); } } return list; } public static bool IsMpeg2PackHeader(byte[] buffer) { return buffer.Length >= 4 && buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 1 && buffer[3] == 0xba; // 0xba == 186 - MPEG-2 Pack Header } public static bool IsPrivateStream1(byte[] buffer, int index) { return buffer.Length >= index + 4 && buffer[index + 0] == 0 && buffer[index + 1] == 0 && buffer[index + 2] == 1 && buffer[index + 3] == 0xbd; // 0xbd == 189 - MPEG-2 Private stream 1 (non MPEG audio, subpictures) } public static bool IsPrivateStream2(byte[] buffer, int index) { return buffer.Length >= index + 4 && buffer[index + 0] == 0 && buffer[index + 1] == 0 && buffer[index + 2] == 1 && buffer[index + 3] == 0xbf; // 0xbf == 191 - MPEG-2 Private stream 2 } public static bool IsSubtitlePack(byte[] buffer) { if (IsMpeg2PackHeader(buffer) && IsPrivateStream1(buffer, Mpeg2Header.Length)) { int pesHeaderDataLength = buffer[Mpeg2Header.Length + 8]; int streamId = buffer[Mpeg2Header.Length + 8 + 1 + pesHeaderDataLength]; if (streamId >= 0x20 && streamId <= 0x3f) // Subtitle IDs allowed (or x3f to x40?) return true; } return false; } } }