using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Diagnostics;
namespace MatrixIO.IO.Bmff
{
public abstract class Box
{
protected Stream _SourceStream;
public Stream SourceStream { get { return _SourceStream; } }
public ulong? Offset { get; protected set; }
public ulong? ContentOffset { get; protected set; }
public ulong? ContentSize
{
get
{
if (!Offset.HasValue || !ContentOffset.HasValue) return null;
if (this is ISuperBox) return null;
return EffectiveSize - (ContentOffset - Offset);
}
}
public bool HasContent
{
get
{
if (ContentSize.HasValue && ContentSize > 0) return true;
else return false;
}
}
public uint Size { get; protected set; }
public ulong? LargeSize { get; protected set; }
public ulong EffectiveSize
{
get
{
if (Size == 0)
{
if (_SourceStream == null) return long.MaxValue;
try
{
return (ulong)_SourceStream.Length;
}
catch (NotSupportedException)
{
return long.MaxValue;
}
}
else if (Size != 1) return Size;
else return LargeSize.HasValue ? LargeSize.Value : 0;
}
protected set
{
if (value > uint.MaxValue)
{
Size = 1;
LargeSize = (ulong)value;
}
else
{
Size = (uint)value;
LargeSize = null;
}
}
}
///
/// Calaculates the serialized size of the Box and its contents.
///
/// Length of the Box header and contents.
internal virtual ulong CalculateSize()
{
ulong calculatedSize = (ulong)(Size == 1 ? 16 : 8);
if (this is ISuperBox)
foreach (Box box in ((ISuperBox)this).Children) calculatedSize += box.CalculateSize();
// TODO: CalculateSize for IContentBox will have to change one the event model is done.
if (this is IContentBox)
{
if (ContentSize.HasValue) calculatedSize += ContentSize.Value;
else if (SourceStream != null && SourceStream.CanSeek) calculatedSize += ContentOffset.HasValue ? (ulong)SourceStream.Length - ContentOffset.Value : (ulong)SourceStream.Length;
else return 0;
}
return calculatedSize;
}
public BoxType Type { get; protected set; }
protected Box()
{
BoxAttribute[] boxAttributes = (BoxAttribute[])this.GetType().GetCustomAttributes(typeof(BoxAttribute), true);
if (boxAttributes != null && boxAttributes.Length != 0) Type = boxAttributes[0].Type;
else if (!(this is Boxes.UnknownBox))
throw new Exception("BMFF Box derivative is not decorated with a BoxAttribute.");
}
///
/// Deserializes a specific box type from the stream.
///
/// Thrown when the next box in the stream is not the expected type.
/// Stream containing the box to be deserialized at the current position.
protected Box(Stream stream)
{
Offset = (ulong)stream.Position;
Size = stream.ReadBEUInt32();
uint type = stream.ReadBEUInt32();
if(Size == 1) LargeSize = stream.ReadBEUInt64();
if (type == 0x75756964) // 'uuid'
{
Type = new BoxType(new Guid(stream.ReadBytes(16)));
}
else Type = new BoxType(type);
bool foundMatchingAttribute = false;
object[] boxAttributes = this.GetType().GetCustomAttributes(typeof(BoxAttribute), true);
foreach (BoxAttribute boxAttribute in boxAttributes) if (boxAttribute.Type == Type) foundMatchingAttribute = true;
if(!foundMatchingAttribute)
throw new FormatException("Unexpected BMFF Box Type.");
Initialize(stream);
}
internal void Initialize(Stream stream, BaseMediaOptions options = BaseMediaOptions.LoadChildren)
{
Trace.WriteLine(Type, "Loading");
_SourceStream = stream;
ConstrainedStream constrainedStream = ConstrainedStream.WrapStream(stream);
constrainedStream.PushConstraint((long)Offset.Value, (long)EffectiveSize);
LoadFromStream(stream);
ContentOffset = (ulong)stream.Position;
if(((options & BaseMediaOptions.LoadChildren) == BaseMediaOptions.LoadChildren) && this is ISuperBox)
LoadChildrenFromStream(stream);
Sync(stream, !(this is IContentBox));
constrainedStream.PopConstraint();
}
///
/// Seek or read ahead to the beginning of the next Box.
///
///
///
protected internal void Sync(Stream stream, bool warn = true)
{
ulong targetPosition = Offset.Value + EffectiveSize;
if (stream.CanSeek)
{
long position = stream.Position;
if (position < (long)targetPosition)
{
if (warn) Trace.WriteLine("Failed to read all bytes in " + this + " box. Seeking ahead.", "WARNING");
stream.Seek((long)targetPosition, SeekOrigin.Begin);
}
}
else
{
// TODO: do this in blocks for performance reasons
while ((ulong)stream.Position < targetPosition) stream.ReadOneByte();
}
}
protected virtual void LoadFromStream(Stream stream) { }
protected virtual void LoadChildrenFromStream(Stream stream)
{
Trace.Indent();
while ((ulong)stream.Position < Offset + EffectiveSize)
{
Box box = Box.FromStream(stream);
if (box != null) ((ISuperBox)this).Children.Add(box);
else break;
}
Trace.Unindent();
}
protected virtual void SaveToStream(Stream stream) { }
protected virtual void SaveChildrenToStream(Stream stream)
{
Trace.Indent();
foreach (Box box in ((ISuperBox)this).Children)
{
box.ToStream(stream);
}
Trace.Unindent();
}
public static T FromStream(Stream stream) where T: Box
{
return (T)FromStream(stream);
}
public static Box FromStream(Stream stream, BaseMediaOptions options = BaseMediaOptions.LoadChildren)
{
Box box = null;
try
{
ulong offset = (ulong)stream.Position;
uint size = stream.ReadBEUInt32();
uint type = stream.ReadBEUInt32();
ulong? largeSize = null;
if(size == 1) largeSize = stream.ReadBEUInt64();
BoxType boxType;
if (type == 0x75756964) // 'uuid'
{
boxType = new BoxType(new Guid(stream.ReadBytes(16)));
}
else boxType = new BoxType(type);
Type t = null;
AvailableBoxTypes.TryGetValue(boxType, out t);
box = t != null ? (Box)Activator.CreateInstance(t) : new Boxes.UnknownBox(boxType);
box.Size = size;
box.LargeSize = largeSize;
box.Offset = offset;
box.Initialize(ConstrainedStream.WrapStream(stream), options);
}
catch (EndOfStreamException) { }
return box;
}
public static IDictionary AvailableBoxTypes { get; private set; }
static Box()
{
AvailableBoxTypes = new Dictionary();
foreach (var boxType in GetBoxTypes(Assembly.GetExecutingAssembly()))
{
AvailableBoxTypes.Add(boxType);
}
Debug.WriteLine("Available Box Types: " + AvailableBoxTypes.Count);
}
private static IEnumerable> GetBoxTypes(Assembly assembly)
{
foreach (Type type in assembly.GetTypes())
{
foreach (BoxAttribute boxAttribute in type.GetCustomAttributes(typeof(BoxAttribute), true))
{
yield return new KeyValuePair(boxAttribute.Type, type);
}
}
}
///
/// Locates all types in the specified assembly decorated with BoxAttribute and adds them
/// to the Box.AvailableBoxTypes collection.
///
/// The assembly to search for new Boxes.
public static void AddBoxTypesFromAssembly(Assembly assembly)
{
foreach(KeyValuePair typeMapping in GetBoxTypes(assembly))
{
if (AvailableBoxTypes.ContainsKey(typeMapping.Key))
AvailableBoxTypes[typeMapping.Key] = typeMapping.Value;
else
AvailableBoxTypes.Add(typeMapping.Key, typeMapping.Value);
}
Debug.WriteLine("Available Box Types: " + AvailableBoxTypes.Count);
}
public void ToStream(Stream stream)
{
Trace.WriteLine(Type, "Saving");
ConstrainedStream constrainedStream = ConstrainedStream.WrapStream(stream);
long offset = constrainedStream.Position;
ulong calculatedLength = CalculateSize() + (ulong)(Size==1 ? 0 : 8);
uint size=1;
ulong largeSize=0;
if (calculatedLength > uint.MaxValue) largeSize = (ulong)calculatedLength;
else size = (uint)calculatedLength - 8;
constrainedStream.PushConstraint(size);
constrainedStream.WriteBEUInt32(size);
constrainedStream.WriteBEUInt32(Type.FourCC);
if (Size == 1) constrainedStream.WriteBEUInt64(largeSize);
if (Type.FourCC == 0x75756964) constrainedStream.WriteBytes(Type.UserType.ToByteArray());
SaveToStream(constrainedStream);
if(this is ISuperBox) SaveChildrenToStream(constrainedStream);
// TODO: Handle IContentBox properly
if (this is IContentBox && this.HasContent)
{
// TODO: Support unseekable streams for the case of Size=0
if (_SourceStream != null && _SourceStream.CanSeek)
{
ConstrainedStream source = new ConstrainedStream(_SourceStream);
source.PushConstraint((long)this.ContentOffset.Value, (long)this.ContentSize.Value);
source.Seek((long)this.ContentOffset.Value, SeekOrigin.Begin);
ulong remaining = this.ContentSize.Value;
byte[] buffer = new byte[4096];
int len = 0;
do
{
len = source.Read(buffer, 0, (ulong)buffer.Length < remaining ? buffer.Length : (int)remaining);
remaining -= (ulong)len;
constrainedStream.Write(buffer, 0, len);
}
while (len == buffer.Length);
}
}
long remainder = offset + (size == 1 ? (long)largeSize : size) - constrainedStream.Position;
if (remainder > 0)
{
Trace.WriteLine("Undershot by " + remainder + " bytes. Padding with 0s.", "WARNING");
for (long i = 0; i < remainder; i++)
constrainedStream.WriteByte(0);
}
constrainedStream.PopConstraint();
}
public Stream GetContentStream()
{
Stream source = ConstrainedStream.UnwrapStream(_SourceStream);
if (!source.CanSeek) throw new FileNotFoundException("Invalid Source Stream in Box.GetContentStream()");
source.Seek((long)ContentOffset, SeekOrigin.Begin);
ConstrainedStream contentStream = ConstrainedStream.WrapStream(source);
contentStream.PushConstraint((long)ContentOffset, (long)ContentSize);
return contentStream;
}
public override string ToString()
{
if (Type.FourCC == 0x75756964) // return formatted guid for 'uuid'
return Type.UserType.ToString("D");
else
return Type.FourCC;
}
}
}