mirror of
https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git
synced 2024-11-24 14:36:13 +00:00
2666 lines
122 KiB
C#
2666 lines
122 KiB
C#
using System;
|
|
using System.IO;
|
|
using ATL.Logging;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using Commons;
|
|
using static ATL.ChannelsArrangements;
|
|
using static ATL.AudioData.FileStructureHelper;
|
|
using System.Linq;
|
|
using System.Collections.Concurrent;
|
|
using static ATL.TagData;
|
|
|
|
namespace ATL.AudioData.IO
|
|
{
|
|
/// <summary>
|
|
/// Class for MP4 files manipulation (extensions : .MP4, .M4A, .M4B, .M4V, .M4P, .M4R, .AAX)
|
|
///
|
|
/// Implementation notes
|
|
/// - If the UDTA atom is absent as a direct child to the MOOV atom, ATL seeks the first TRAK that has an UDTA atom
|
|
/// and considers that one as the entire file's metadata
|
|
///
|
|
/// - When removing a Track, physical chunks belonging to the track (i.e. those indexed by 'stco') won't be removed
|
|
///
|
|
/// - When adding the exact same chapter picture to multiple chapters, that picture is written as many times as there are chapters
|
|
/// instead of being written once and referenced from each chapter
|
|
///
|
|
/// </summary>
|
|
class MP4 : MetaDataIO, IAudioDataIO
|
|
{
|
|
// Bit rate type codes
|
|
public const byte MP4_BITRATE_TYPE_UNKNOWN = 0; // Unknown
|
|
public const byte MP4_BITRATE_TYPE_CBR = 1; // CBR
|
|
public const byte MP4_BITRATE_TYPE_VBR = 2; // VBR
|
|
|
|
// de facto default namespace for custom fields
|
|
private const string DEFAULT_NAMESPACE = "com.apple.iTunes";
|
|
|
|
private static readonly byte[] FILE_HEADER = Utils.Latin1Encoding.GetBytes("ftyp");
|
|
|
|
private static readonly byte[] ILST_CORE_SIGNATURE = { 0, 0, 0, 8, 105, 108, 115, 116 }; // (int32)8 followed by "ilst" field code
|
|
|
|
private const string ZONE_MP4_NOUDTA = "noudta"; // Placeholder for missing 'udta' atom
|
|
private const string ZONE_MP4_NOMETA = "nometa"; // Placeholder for missing 'meta' atom
|
|
private const string ZONE_MP4_ILST = "ilst"; // When editing a file with an existing 'meta' atom
|
|
private const string ZONE_MP4_CHPL = "chpl"; // Nero chapters
|
|
private const string ZONE_MP4_XTRA = "Xtra"; // Specific fields (e.g. rating) inserted by Microsoft instead of using standard MP4 fields
|
|
private const string ZONE_MP4_QT_CHAP_NOTREF = "qt_notref"; // Placeholder for missing track reference atom
|
|
private const string ZONE_MP4_QT_CHAP_CHAP = "qt_chap_chap"; // Quicktime chapters track reference
|
|
private const string ZONE_MP4_QT_CHAP_TXT_TRAK = "qt_trak_txt"; // Quicktime chapters text track
|
|
private const string ZONE_MP4_QT_CHAP_PIC_TRAK = "qt_trak_pic"; // Quicktime chapters picture track
|
|
private const string ZONE_MP4_QT_CHAP_MDAT = "qt_chap_mdat"; // Quicktime chapters data
|
|
private const string ZONE_MP4_ADDITIONAL_UUIDS = "uuids"; // Zone to write extra UUIDs
|
|
|
|
private const string ZONE_MP4_PHYSICAL_CHUNK = "chunk"; // Physical audio chunk referenced from stco or co64
|
|
|
|
// Mapping between MP4 frame codes and ATL frame codes
|
|
private static readonly Dictionary<string, Field> frameMapping_mp4 = new Dictionary<string, Field>() {
|
|
{ "©nam", Field.TITLE },
|
|
{ "titl", Field.TITLE },
|
|
{ "©alb", Field.ALBUM },
|
|
{ "©ART", Field.ARTIST },
|
|
{ "©art", Field.ARTIST },
|
|
{ "©cmt", Field.COMMENT },
|
|
{ "©day", Field.RECORDING_DATE_OR_YEAR },
|
|
{ "©gen", Field.GENRE },
|
|
{ "gnre", Field.GENRE },
|
|
{ "trkn", Field.TRACK_NUMBER_TOTAL },
|
|
{ "disk", Field.DISC_NUMBER_TOTAL },
|
|
{ "rtng", Field.RATING },
|
|
{ "rate", Field.RATING },
|
|
{ "©wrt", Field.COMPOSER },
|
|
{ "desc", Field.GENERAL_DESCRIPTION }, // Description
|
|
{ "©des", Field.GENERAL_DESCRIPTION }, // Long description
|
|
{ "cprt", Field.COPYRIGHT },
|
|
{ "aART", Field.ALBUM_ARTIST },
|
|
{ "©lyr", Field.LYRICS_UNSYNCH },
|
|
{ "©pub", Field.PUBLISHER },
|
|
{ "rldt", Field.PUBLISHING_DATE},
|
|
{ "prID", Field.PRODUCT_ID},
|
|
{ "©con", Field.CONDUCTOR },
|
|
{ "CONDUCTOR", Field.CONDUCTOR }, // aka ----:com.apple.iTunes:CONDUCTOR
|
|
{ "soal", Field.SORT_ALBUM },
|
|
{ "soaa", Field.SORT_ALBUM_ARTIST },
|
|
{ "soar", Field.SORT_ARTIST },
|
|
{ "sonm", Field.SORT_TITLE },
|
|
{ "©grp", Field.GROUP },
|
|
{ "©mvi", Field.SERIES_PART},
|
|
{ "©mvn", Field.SERIES_TITLE },
|
|
{ "ldes", Field.LONG_DESCRIPTION },
|
|
{ "tmpo", Field.BPM },
|
|
{ "©enc", Field.ENCODED_BY },
|
|
{ "©too", Field.ENCODER },
|
|
{ "LANGUAGE", Field.LANGUAGE }, // aka ----:com.apple.iTunes:LANGUAGE
|
|
{ "©isr", Field.ISRC },
|
|
{ "CATALOGNUMBER", Field.CATALOG_NUMBER }, // aka ----:com.apple.iTunes:CATALOGNUMBER
|
|
{ "LYRICIST", Field.LYRICIST } // aka ----:com.apple.iTunes:LYRICIST
|
|
};
|
|
|
|
// Mapping between MP4 frame codes and frame classes that aren't class 1 (UTF-8 text)
|
|
// 0 = special / 21 = int8-16-24-32 / 22 = uint8-16-24-32
|
|
private static readonly ConcurrentDictionary<string, byte> frameClasses_mp4 = new ConcurrentDictionary<string, byte>()
|
|
{
|
|
["gnre"] = 0,
|
|
["trkn"] = 0,
|
|
["disk"] = 0,
|
|
["rtng"] = 21,
|
|
["tmpo"] = 21,
|
|
["cpil"] = 21,
|
|
["stik"] = 21,
|
|
["pcst"] = 21,
|
|
["purl"] = 0,
|
|
["egid"] = 0,
|
|
["tvsn"] = 22,
|
|
["tves"] = 21,
|
|
["pgap"] = 21,
|
|
["shwm"] = 21,
|
|
["hdvd"] = 21,
|
|
["©mvc"] = 21,
|
|
["©mvi"] = 21
|
|
};
|
|
|
|
private sealed class MP4Sample
|
|
{
|
|
public double Duration;
|
|
public uint Size;
|
|
public uint ChunkIndex; // 1-based index
|
|
public long ChunkOffset;
|
|
public long RelativeOffset;
|
|
}
|
|
|
|
private sealed class Uuid
|
|
{
|
|
public long position;
|
|
public uint size;
|
|
public string key = "";
|
|
public string value = "";
|
|
|
|
public bool isValid()
|
|
{
|
|
return 32 == key.Length && Utils.IsHex(key);
|
|
}
|
|
}
|
|
|
|
// Inner technical information to remember for writing purposes
|
|
private uint globalTimeScale;
|
|
private readonly IDictionary<int, int> trackTimescales = new Dictionary<int, int>();
|
|
private int qtChapterTextTrackId;
|
|
private int qtChapterPictureTrackId;
|
|
private long initialPaddingOffset;
|
|
private uint initialPaddingSize;
|
|
private byte[] chapterTextTrackEdits;
|
|
private byte[] chapterPictureTrackEdits;
|
|
private long udtaOffset;
|
|
|
|
private byte bitrateTypeID;
|
|
private double bitrate;
|
|
private double calculatedDurationMs; // Calculated track duration, in milliseconds
|
|
|
|
private AudioDataManager.SizeInfo sizeInfo;
|
|
|
|
|
|
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
|
|
|
|
// IAudioDataIO
|
|
public bool IsVBR => MP4_BITRATE_TYPE_VBR == bitrateTypeID;
|
|
|
|
public Format AudioFormat
|
|
{
|
|
get;
|
|
}
|
|
public int CodecFamily => AudioDataIOFactory.CF_LOSSY;
|
|
|
|
public double BitRate => bitrate / 1000.0;
|
|
public int BitDepth => -1; // Irrelevant for lossy formats
|
|
public double Duration => getDuration();
|
|
|
|
public int SampleRate { get; private set; }
|
|
|
|
public string FileName { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
|
|
{
|
|
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE, MetaDataIOFactory.TagType.APE, MetaDataIOFactory.TagType.ID3V1 };
|
|
}
|
|
|
|
public ChannelsArrangement ChannelsArrangement { get; private set; }
|
|
|
|
public long AudioDataOffset { get; set; }
|
|
public long AudioDataSize { get; set; }
|
|
|
|
|
|
// IMetaDataIO
|
|
protected override int getDefaultTagOffset()
|
|
{
|
|
return TO_BUILTIN;
|
|
}
|
|
protected override MetaDataIOFactory.TagType getImplementedTagType()
|
|
{
|
|
return MetaDataIOFactory.TagType.NATIVE;
|
|
}
|
|
public override byte FieldCodeFixedLength => 4;
|
|
|
|
protected override bool isLittleEndian => false;
|
|
|
|
protected override byte ratingConvention => RC_APE;
|
|
|
|
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
|
|
{
|
|
Field supportedMetaId = Field.NO_FIELD;
|
|
|
|
if (frameMapping_mp4.TryGetValue(ID, out var value)) supportedMetaId = value;
|
|
|
|
return supportedMetaId;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override bool canHandleNonStandardField(string code, string value)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
|
|
// ---------- CONSTRUCTORS & INITIALIZERS
|
|
|
|
protected void resetData()
|
|
{
|
|
bitrateTypeID = MP4_BITRATE_TYPE_UNKNOWN;
|
|
|
|
globalTimeScale = 0;
|
|
trackTimescales.Clear();
|
|
qtChapterTextTrackId = 0;
|
|
qtChapterPictureTrackId = 0;
|
|
initialPaddingSize = 0;
|
|
initialPaddingOffset = -1;
|
|
AudioDataOffset = -1;
|
|
AudioDataSize = 0;
|
|
udtaOffset = 0;
|
|
|
|
bitrate = 0;
|
|
SampleRate = 0;
|
|
calculatedDurationMs = 0;
|
|
|
|
chapterTextTrackEdits = null;
|
|
chapterPictureTrackEdits = null;
|
|
|
|
ResetData();
|
|
}
|
|
|
|
public MP4(string fileName, Format format)
|
|
{
|
|
this.FileName = fileName;
|
|
AudioFormat = format;
|
|
resetData();
|
|
}
|
|
|
|
// ********************** Private functions & procedures *********************
|
|
|
|
private static void addFrameClass(string frameCode, byte frameClass)
|
|
{
|
|
frameClasses_mp4.TryAdd(frameCode, frameClass);
|
|
}
|
|
|
|
// Calculate duration time
|
|
private double getDuration()
|
|
{
|
|
return calculatedDurationMs;
|
|
}
|
|
|
|
public static bool IsValidHeader(byte[] data)
|
|
{
|
|
// Examine bytes 4 to 8
|
|
byte[] usefulData = new byte[4];
|
|
Array.Copy(data, 4, usefulData, 0, 4);
|
|
return StreamUtils.ArrBeginsWith(usefulData, FILE_HEADER);
|
|
}
|
|
|
|
// Get header type of the file
|
|
private bool recognizeHeaderType(BinaryReader Source)
|
|
{
|
|
Source.BaseStream.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
|
|
return IsValidHeader(Source.ReadBytes(8));
|
|
}
|
|
|
|
private void readQTChapters(BinaryReader source, IList<MP4Sample> chapterTextTrackSamples, IList<MP4Sample> chapterPictureTrackSamples)
|
|
{
|
|
tagExists = true;
|
|
if (3 == Settings.MP4_readChaptersFormat) return;
|
|
|
|
if (null == tagData.Chapters) tagData.Chapters = new List<ChapterInfo>(); else tagData.Chapters.Clear();
|
|
double cumulatedDuration = 0;
|
|
|
|
// Text chapters are "master data"; picture chapters get attached to them
|
|
for (int i = 0; i < chapterTextTrackSamples.Count; i++)
|
|
{
|
|
MP4Sample textSample = chapterTextTrackSamples[i];
|
|
MP4Sample pictureSample = i < chapterPictureTrackSamples.Count ? chapterPictureTrackSamples[i] : null;
|
|
if (textSample.ChunkOffset > 0)
|
|
{
|
|
ChapterInfo chapter = new ChapterInfo();
|
|
|
|
source.BaseStream.Seek(textSample.ChunkOffset + textSample.RelativeOffset, SeekOrigin.Begin);
|
|
ushort strDataSize = StreamUtils.DecodeBEUInt16(source.ReadBytes(2));
|
|
|
|
chapter.Title = Encoding.UTF8.GetString(source.ReadBytes(strDataSize));
|
|
chapter.StartTime = (uint)Math.Round(cumulatedDuration);
|
|
cumulatedDuration += textSample.Duration * 1000;
|
|
chapter.EndTime = (uint)Math.Round(cumulatedDuration);
|
|
|
|
if (pictureSample != null && pictureSample.ChunkOffset > 0 && pictureSample.Size > 0)
|
|
{
|
|
source.BaseStream.Seek(pictureSample.ChunkOffset + pictureSample.RelativeOffset, SeekOrigin.Begin);
|
|
byte[] data = new byte[pictureSample.Size];
|
|
source.Read(data, 0, (int)pictureSample.Size);
|
|
chapter.Picture = PictureInfo.fromBinaryData(data, PictureInfo.PIC_TYPE.Generic, getImplementedTagType());
|
|
}
|
|
|
|
tagData.Chapters.Add(chapter);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read MP4 header data
|
|
/// http://www.jiscdigitalmedia.ac.uk/guide/aac-audio-and-the-mp4-media-format
|
|
/// http://atomicparsley.sourceforge.net/mpeg-4files.html
|
|
/// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
|
|
/// - Metadata is located in the moov/udta/meta/ilst atom
|
|
/// - Physical metadata are located in the moov/trak atoms
|
|
/// - Binary physical data are located in the mdat atoms
|
|
/// </summary>
|
|
/// <param name="source">Source to read from</param>
|
|
/// <param name="readTagParams">Reading parameters</param>
|
|
private bool readMP4(BinaryReader source, ReadTagParams readTagParams)
|
|
{
|
|
byte[] data32 = new byte[4];
|
|
|
|
IList<long> audioTrackOffsets = new List<long>(); // Offset of all detected audio/video tracks (tracks with a media type of 'soun' or 'vide')
|
|
IList<MP4Sample> chapterTextTrackSamples = new List<MP4Sample>(); // If non-empty, quicktime chapters have been detected
|
|
IList<MP4Sample> chapterPictureTrackSamples = new List<MP4Sample>(); // If non-empty, quicktime chapters have been detected
|
|
IDictionary<int, IList<int>> chapterTrackIndexes = new Dictionary<int, IList<int>>(); // Key is track index (1-based); lists are chapter tracks indexes (1-based)
|
|
|
|
|
|
// TODO PERF - try and cache the whole tree structure to optimize browsing through nodes
|
|
|
|
source.BaseStream.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
|
|
|
|
// FTYP atom
|
|
source.Read(data32, 0, 4);
|
|
var atomSize = StreamUtils.DecodeBEUInt32(data32);
|
|
source.BaseStream.Seek(atomSize - 4, SeekOrigin.Current);
|
|
|
|
// MOOV atom
|
|
uint moovSize = navigateToAtom(source, "moov"); // === Physical data
|
|
if (0 == moovSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "moov atom could not be found; aborting read");
|
|
return false;
|
|
}
|
|
|
|
var moovPosition = source.BaseStream.Position;
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_NOUDTA);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_NOMETA);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_ILST);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_XTRA);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_CHPL);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_QT_CHAP_PIC_TRAK);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_QT_CHAP_TXT_TRAK);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_QT_CHAP_NOTREF);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, ZONE_MP4_QT_CHAP_CHAP);
|
|
}
|
|
|
|
// === Physical data header
|
|
if (0 == navigateToAtom(source.BaseStream, "mvhd"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "mvhd atom could not be found; aborting read");
|
|
return false;
|
|
}
|
|
byte version = source.ReadByte();
|
|
source.BaseStream.Seek(3, SeekOrigin.Current); // 3-byte flags
|
|
if (1 == version) source.BaseStream.Seek(16, SeekOrigin.Current);
|
|
else source.BaseStream.Seek(8, SeekOrigin.Current);
|
|
|
|
globalTimeScale = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
long timeLengthPerSec;
|
|
if (1 == version) timeLengthPerSec = StreamUtils.DecodeBEInt64(source.ReadBytes(8));
|
|
else timeLengthPerSec = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
calculatedDurationMs = timeLengthPerSec * 1000.0 / globalTimeScale;
|
|
|
|
long trackCounterPosition = source.BaseStream.Position + 76;
|
|
|
|
source.BaseStream.Seek(moovPosition, SeekOrigin.Begin);
|
|
byte currentTrakIndex = 0;
|
|
long trakSize;
|
|
|
|
// Loop through tracks
|
|
do
|
|
{
|
|
trakSize = readTrack(source, readTagParams, ++currentTrakIndex, chapterTextTrackSamples, chapterPictureTrackSamples, chapterTrackIndexes, audioTrackOffsets, trackCounterPosition, moovPosition, moovSize);
|
|
if (-1 == trakSize)
|
|
{
|
|
// TODO do better than that
|
|
currentTrakIndex = 0; // Convention to start reading from index 1 again
|
|
source.BaseStream.Seek(moovPosition, SeekOrigin.Begin);
|
|
trakSize = 1;
|
|
}
|
|
}
|
|
while (trakSize > 0);
|
|
|
|
// Look for uuid atoms
|
|
source.BaseStream.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
|
|
Uuid uuid;
|
|
do
|
|
{
|
|
uuid = readUuid(source.BaseStream);
|
|
if (uuid.isValid())
|
|
{
|
|
tagExists = true;
|
|
if (XmpTag.UUID_XMP == uuid.key)
|
|
{
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(uuid.value));
|
|
XmpTag.FromStream(stream, this, readTagParams, stream.Length);
|
|
}
|
|
else
|
|
{
|
|
SetMetaField("uuid." + uuid.key, uuid.value, readTagParams.ReadAllMetaFrames);
|
|
}
|
|
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
structureHelper.AddZone(uuid.position, uuid.size, "uuid." + uuid.key);
|
|
}
|
|
}
|
|
} while (uuid.size > 0);
|
|
|
|
|
|
// Seek audio data segment to calculate mean bitrate
|
|
// NB : This figure is closer to truth than the "average bitrate" recorded in the esds/m4ds header
|
|
|
|
// == Audio binary data, chapter or subtitle data
|
|
// Per convention, audio binary data always seems to be in the 1st mdat atom of the file
|
|
source.BaseStream.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
|
|
var mdatAtomData = navigateToAtomSize(source.BaseStream, "mdat");
|
|
uint mdatSize = mdatAtomData.Item1;
|
|
int mdatHeaderSize = mdatAtomData.Item2;
|
|
if (0 == mdatSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "mdat atom could not be found; aborting read");
|
|
return false;
|
|
}
|
|
long mdatOffset = source.BaseStream.Position;
|
|
AudioDataOffset = mdatOffset - mdatHeaderSize;
|
|
AudioDataSize = mdatSize;
|
|
bitrate = (int)Math.Round(mdatSize * 8 / calculatedDurationMs * 1000.0, 0);
|
|
|
|
// Set zone for new UUIDs to write at the end of the file
|
|
if (readTagParams.PrepareForWriting) structureHelper.AddZone(AudioDataOffset + AudioDataSize, 0, ZONE_MP4_ADDITIONAL_UUIDS);
|
|
|
|
|
|
// == Quicktime chapters management
|
|
|
|
// No QT chapter track found -> Assign free track ID
|
|
if (0 == qtChapterTextTrackId)
|
|
{
|
|
qtChapterTextTrackId = currentTrakIndex++;
|
|
trackTimescales[qtChapterTextTrackId] = 1000; // Easier to encode base 10 timecodes
|
|
}
|
|
if (0 == qtChapterPictureTrackId)
|
|
{
|
|
qtChapterPictureTrackId = currentTrakIndex;
|
|
trackTimescales[qtChapterPictureTrackId] = 1000; // Easier to encode base 10 timecodes
|
|
}
|
|
|
|
// QT chapters have been detected while browsing tracks
|
|
if (chapterTextTrackSamples.Count > 0) readQTChapters(source, chapterTextTrackSamples, chapterPictureTrackSamples);
|
|
|
|
// If QT chapters data is missing, reserve zones to write QT chapters
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
// Candidates for chapters MDAT zone
|
|
// NB : limit zone size to the actual size of the chapters
|
|
long chapMdatOffset = -1; // Offset of the MDAT atom hosting chapters
|
|
long chapMdatDataSize = -1; // Size of chapters data inside the MDAT atom
|
|
uint chapMdatChapSize = 0; // Size of the entire MDAT atom (to properly rewrite the zone size header)
|
|
|
|
if (Settings.MP4_createQuicktimeChapters && (0 == chapterTextTrackSamples.Count || 0 == chapterPictureTrackSamples.Count))
|
|
{
|
|
source.BaseStream.Seek(moovPosition, SeekOrigin.Begin); // TRAK before UDTA
|
|
atomSize = navigateToAtom(source, "udta");
|
|
if (atomSize > 0)
|
|
{
|
|
if (0 == chapterTextTrackSamples.Count) structureHelper.AddZone(source.BaseStream.Position - 8, 0, ZONE_MP4_QT_CHAP_TXT_TRAK);
|
|
if (0 == chapterPictureTrackSamples.Count) structureHelper.AddZone(source.BaseStream.Position - 8, 0, ZONE_MP4_QT_CHAP_PIC_TRAK);
|
|
}
|
|
|
|
// By default, attach to-be text and image data to the first MDAT atom
|
|
chapMdatOffset = AudioDataOffset;
|
|
chapMdatDataSize = 0;
|
|
chapMdatChapSize = mdatSize;
|
|
}
|
|
|
|
// If QT chapters are present, record the current zone for chapters data
|
|
if (chapterTextTrackSamples.Count > 0 && (Settings.MP4_keepExistingChapters || Settings.MP4_createQuicktimeChapters))
|
|
{
|
|
long minChapterOffset = chapterTextTrackSamples.Min(sample => sample.ChunkOffset);
|
|
|
|
// Detect if QT chapters are interleaved
|
|
long previousEndOffset = 0;
|
|
foreach (MP4Sample sample in chapterTextTrackSamples)
|
|
{
|
|
if (0 == previousEndOffset) previousEndOffset = sample.ChunkOffset + sample.RelativeOffset + sample.Size;
|
|
else if (previousEndOffset == sample.ChunkOffset + sample.RelativeOffset)
|
|
{
|
|
previousEndOffset = sample.ChunkOffset + sample.RelativeOffset + sample.Size;
|
|
}
|
|
else
|
|
{
|
|
structureHelper.RemoveZone(ZONE_MP4_QT_CHAP_NOTREF);
|
|
structureHelper.RemoveZone(ZONE_MP4_QT_CHAP_CHAP);
|
|
structureHelper.RemoveZone(ZONE_MP4_QT_CHAP_TXT_TRAK);
|
|
structureHelper.RemoveZone(ZONE_MP4_QT_CHAP_PIC_TRAK);
|
|
structureHelper.RemoveZone(ZONE_MP4_QT_CHAP_MDAT);
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "ATL does not support writing non-contiguous (e.g. interleaved with audio data) Quicktime chapters; ignoring Quicktime chapters.");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Scan all MDAT atoms starting from the first one to detect the one containing existing chapters
|
|
source.BaseStream.Seek(mdatOffset, SeekOrigin.Begin);
|
|
long chapterTextSize = chapterTextTrackSamples.Sum(sample => sample.Size);
|
|
long chapterPictureSize = chapterPictureTrackSamples.Sum(sample => sample.Size);
|
|
do
|
|
{
|
|
// On some files, there's a single MDAT atom that contains both chapter references and audio data
|
|
// => limit zone size to the actual size of the chapters
|
|
// TODO handle non-contiguous chapters (e.g. chapter data interleaved with audio data)
|
|
if (minChapterOffset >= source.BaseStream.Position && minChapterOffset < source.BaseStream.Position - mdatHeaderSize + mdatSize)
|
|
{
|
|
chapMdatOffset = source.BaseStream.Position - mdatHeaderSize;
|
|
// Zone size = size of chapter data (text and pictures)
|
|
chapMdatDataSize = chapterTextSize + chapterPictureSize;
|
|
chapMdatChapSize = mdatSize;
|
|
}
|
|
|
|
source.BaseStream.Seek(mdatSize - mdatHeaderSize, SeekOrigin.Current);
|
|
mdatAtomData = navigateToAtomSize(source.BaseStream, "mdat");
|
|
mdatSize = mdatAtomData.Item1;
|
|
mdatHeaderSize = mdatAtomData.Item2;
|
|
} while (mdatSize > 0);
|
|
} // QT chapters are present
|
|
|
|
// Memorize the definitive chapter data location as a zone
|
|
if (chapMdatDataSize > -1)
|
|
{
|
|
structureHelper.AddZone(chapMdatOffset + mdatHeaderSize, chapMdatDataSize, ZONE_MP4_QT_CHAP_MDAT);
|
|
structureHelper.AddSize(chapMdatOffset, chapMdatChapSize, ZONE_MP4_QT_CHAP_MDAT);
|
|
}
|
|
} // Write mode
|
|
|
|
|
|
// Read user data which contains metadata and Nero chapters
|
|
readUserData(source, readTagParams, moovPosition, moovSize);
|
|
|
|
// == Padding management
|
|
// Seek the generic padding atom
|
|
source.BaseStream.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
|
|
initialPaddingSize = navigateToAtom(source.BaseStream, "free");
|
|
if (initialPaddingSize > 0) tagData.PaddingSize = initialPaddingSize;
|
|
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
// Padding atom found
|
|
if (initialPaddingSize > 0)
|
|
{
|
|
initialPaddingOffset = source.BaseStream.Position - 8;
|
|
structureHelper.AddZone(source.BaseStream.Position - 8, (int)initialPaddingSize, PADDING_ZONE_NAME);
|
|
structureHelper.AddSize(source.BaseStream.Position - 8, (int)initialPaddingSize, PADDING_ZONE_NAME, PADDING_ZONE_NAME);
|
|
}
|
|
else // Padding atom not found
|
|
{
|
|
if (Settings.AddNewPadding) // Create a virtual position to insert a new padding zone
|
|
{
|
|
structureHelper.AddZone(source.BaseStream.Position - 8, 0, PADDING_ZONE_NAME);
|
|
structureHelper.AddSize(source.BaseStream.Position - 8, 0, PADDING_ZONE_NAME, PADDING_ZONE_NAME);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private long readTrack(
|
|
BinaryReader source,
|
|
ReadTagParams readTagParams,
|
|
int currentTrakIndex,
|
|
IList<MP4Sample> chapterTextTrackSamples,
|
|
IList<MP4Sample> chapterPictureTrackSamples,
|
|
IDictionary<int, IList<int>> chapterTrackIndexes,
|
|
IList<long> mediaTrackOffsets,
|
|
long trackCounterOffset,
|
|
long moovPosition,
|
|
long moovSize
|
|
)
|
|
{
|
|
int mediaTimeScale = 1000;
|
|
|
|
uint int32Data = 0;
|
|
byte[] data32 = new byte[4];
|
|
byte[] data64 = new byte[8];
|
|
|
|
bool isCurrentTrackFirstChapterTextTrack = false; // First chapter text track which should contain chapter titles (as opposed to URLs)
|
|
bool isCurrentTrackOtherChapterTrack = false; // Generic marker for other chapter-related text tracks
|
|
bool isCurrentTrackFirstChapterPicturesTrack = false; // First chapter picture track which should contain chapter "covers"
|
|
bool isCurrentTrackFirstAudioTrack = false;
|
|
|
|
string trackZoneName = "";
|
|
|
|
uint trakSize = navigateToAtom(source.BaseStream, "trak");
|
|
if (0 == trakSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "total tracks found : " + (currentTrakIndex - 1));
|
|
return 0;
|
|
}
|
|
var trakPosition = source.BaseStream.Position - 8;
|
|
|
|
// Read track ID
|
|
if (0 == navigateToAtom(source.BaseStream, "tkhd"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "trak.tkhd atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
int intLength = 0 == source.ReadByte() ? 4 : 8;
|
|
source.BaseStream.Seek(3, SeekOrigin.Current); // Flags
|
|
source.BaseStream.Seek(intLength * 2, SeekOrigin.Current); // Creation & Modification Dates
|
|
|
|
int trackId = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
trackZoneName = "track." + trackId;
|
|
structureHelper.AddZone(trakPosition, 0, trackZoneName, false);
|
|
structureHelper.AddCounter(trackCounterOffset, 1 == trackId ? 2 : 1, trackZoneName);
|
|
}
|
|
|
|
// Detect the track type
|
|
source.BaseStream.Seek(trakPosition + 8, SeekOrigin.Begin);
|
|
if (0 == navigateToAtom(source, "mdia"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "mdia atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
|
|
long mdiaPosition = source.BaseStream.Position;
|
|
if (chapterTrackIndexes.Count > 0)
|
|
{
|
|
if (0 == navigateToAtom(source, "mdhd"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "mdia.mdhd atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
|
|
byte mdhdVersion = source.ReadByte();
|
|
source.BaseStream.Seek(3, SeekOrigin.Current); // Flags
|
|
|
|
if (0 == mdhdVersion) source.BaseStream.Seek(8, SeekOrigin.Current);
|
|
else source.BaseStream.Seek(16, SeekOrigin.Current); // Creation and modification date
|
|
|
|
mediaTimeScale = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
|
|
|
|
source.BaseStream.Seek(mdiaPosition, SeekOrigin.Begin);
|
|
}
|
|
trackTimescales[trackId] = mediaTimeScale;
|
|
|
|
if (0 == navigateToAtom(source, "hdlr"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "mdia.hdlr atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Version and flags
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Quicktime type
|
|
string mediaType = Utils.Latin1Encoding.GetString(source.ReadBytes(4));
|
|
|
|
// Check if current track is the 1st chapter track
|
|
// NB : Per convention, we will admit that the 1st track referenced in the 'chap' atom
|
|
// contains the chapter names (as opposed to chapter URLs or chapter images)
|
|
isCurrentTrackOtherChapterTrack = false;
|
|
if ("text".Equals(mediaType) && chapterTrackIndexes.Count > 0)
|
|
{
|
|
foreach (IList<int> list in chapterTrackIndexes.Values)
|
|
{
|
|
if (trackId == list[0])
|
|
{
|
|
isCurrentTrackFirstChapterTextTrack = true;
|
|
isCurrentTrackOtherChapterTrack = false;
|
|
break;
|
|
}
|
|
foreach (int index in list)
|
|
{
|
|
if (trackId == index)
|
|
{
|
|
isCurrentTrackOtherChapterTrack = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ("soun".Equals(mediaType) || "vide".Equals(mediaType))
|
|
{
|
|
mediaTrackOffsets.Add(trakPosition);
|
|
isCurrentTrackFirstAudioTrack = (1 == mediaTrackOffsets.Count);
|
|
}
|
|
|
|
if (readTagParams.PrepareForWriting && isCurrentTrackOtherChapterTrack && !isCurrentTrackFirstChapterTextTrack)
|
|
{
|
|
structureHelper.RemoveZone(trackZoneName);
|
|
trackZoneName = ZONE_MP4_QT_CHAP_TXT_TRAK + "." + trackId;
|
|
structureHelper.AddZone(trakPosition, (int)trakSize, trackZoneName);
|
|
structureHelper.AddSize(moovPosition - 8, moovSize, trackZoneName);
|
|
structureHelper.AddCounter(trackCounterOffset, (1 == trackId) ? 2 : 1, trackZoneName);
|
|
}
|
|
|
|
source.BaseStream.Seek(mdiaPosition, SeekOrigin.Begin);
|
|
if (0 == navigateToAtom(source, "minf"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "mdia.minf atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
if (0 == navigateToAtom(source, "stbl"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "mdia.minf.stbl atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
long stblPosition = source.BaseStream.Position;
|
|
|
|
// Look for sample rate
|
|
if (0 == navigateToAtom(source, "stsd"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "stsd atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte flags
|
|
uint nbDescriptions = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
|
|
for (int i = 0; i < nbDescriptions; i++)
|
|
{
|
|
int32Data = StreamUtils.DecodeBEUInt32(source.ReadBytes(4)); // 4-byte description length
|
|
string descFormat = Utils.Latin1Encoding.GetString(source.ReadBytes(4));
|
|
|
|
// Descriptors for audio
|
|
if (descFormat.Equals("mp4a") || descFormat.Equals("enca") || descFormat.Equals("samr") || descFormat.Equals("sawb"))
|
|
{
|
|
source.BaseStream.Seek(6, SeekOrigin.Current); // SampleEntry / 6-byte reserved zone set to zero
|
|
source.BaseStream.Seek(2, SeekOrigin.Current); // SampleEntry / Data reference index
|
|
|
|
source.BaseStream.Seek(8, SeekOrigin.Current); // AudioSampleEntry / 8-byte reserved zone
|
|
|
|
ushort channels = StreamUtils.DecodeBEUInt16(source.ReadBytes(2)); // Channel count
|
|
ChannelsArrangement = GuessFromChannelNumber(channels);
|
|
|
|
source.BaseStream.Seek(2, SeekOrigin.Current); // Sample size
|
|
source.BaseStream.Seek(2, SeekOrigin.Current); // Quicktime stuff (should be length 4, but sampleRate doesn't work when it is...)
|
|
|
|
SampleRate = (int)StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
}
|
|
else if (descFormat.Equals("jpeg")) // Descriptor for picture (slides / chapter pictures)
|
|
{
|
|
isCurrentTrackFirstChapterPicturesTrack = chapterTrackIndexes.Values.Any(list => list.Contains(trackId));
|
|
}
|
|
else
|
|
{
|
|
source.BaseStream.Seek(int32Data - 4, SeekOrigin.Current);
|
|
}
|
|
}
|
|
|
|
// Look for "trak.tref.chap" atom to detect QT chapters for current track
|
|
source.BaseStream.Seek(trakPosition + 8, SeekOrigin.Begin);
|
|
uint trefSize = navigateToAtom(source, "tref");
|
|
long trefPosition = source.BaseStream.Position - 8;
|
|
// Existing, non-empty tref atom
|
|
if (trefSize > 8 && 0 == chapterTrackIndexes.Count)
|
|
{
|
|
bool parsePreviousTracks = false;
|
|
uint chapSize = navigateToAtom(source, "chap");
|
|
if (chapSize > 0 && (Settings.MP4_keepExistingChapters || Settings.MP4_createQuicktimeChapters))
|
|
{
|
|
structureHelper.AddZone(source.BaseStream.Position - 8, (int)chapSize, ZONE_MP4_QT_CHAP_CHAP);
|
|
structureHelper.AddSize(trakPosition, trakSize, ZONE_MP4_QT_CHAP_CHAP);
|
|
structureHelper.AddSize(trefPosition, trefSize, ZONE_MP4_QT_CHAP_CHAP);
|
|
}
|
|
if (chapSize > 8)
|
|
{
|
|
IList<int> thisTrackIndexes = new List<int>();
|
|
for (int i = 0; i < (chapSize - 8) / 4; i++)
|
|
{
|
|
thisTrackIndexes.Add(StreamUtils.DecodeBEInt32(source.ReadBytes(4)));
|
|
}
|
|
chapterTrackIndexes.Add(trackId, thisTrackIndexes);
|
|
|
|
foreach (int i in thisTrackIndexes)
|
|
{
|
|
if (i < trackId)
|
|
{
|
|
parsePreviousTracks = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If current track has declared a chapter track located at a previous index, come back to read it
|
|
if (parsePreviousTracks)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_INFO, "detected chapter track located before read cursor; restarting reading tracks from track 1");
|
|
return -1;
|
|
}
|
|
}
|
|
else if (isCurrentTrackFirstAudioTrack && Settings.MP4_createQuicktimeChapters) // Only add QT chapters to the 1st detected audio or video track
|
|
{
|
|
if (0 == trefSize) // No atom at all
|
|
{
|
|
structureHelper.AddZone(trakPosition + trakSize, 0, ZONE_MP4_QT_CHAP_NOTREF);
|
|
structureHelper.AddSize(trakPosition, trakSize, ZONE_MP4_QT_CHAP_NOTREF);
|
|
}
|
|
else if (trefSize <= 8) // Existing empty atom
|
|
{
|
|
structureHelper.AddZone(trefPosition, trefSize, ZONE_MP4_QT_CHAP_NOTREF);
|
|
structureHelper.AddSize(trakPosition, trakSize, ZONE_MP4_QT_CHAP_NOTREF);
|
|
}
|
|
}
|
|
|
|
// Read chapters textual data
|
|
if (isCurrentTrackFirstChapterTextTrack)
|
|
{
|
|
uint result = readQtChapter(source, readTagParams, stblPosition, trakPosition, trakSize, trackId, trackCounterOffset, chapterTextTrackSamples, mediaTimeScale, true);
|
|
if (result > 0) return int32Data; // An error has occured
|
|
}
|
|
|
|
// Read chapters picture data
|
|
if (isCurrentTrackFirstChapterPicturesTrack)
|
|
{
|
|
uint result = readQtChapter(source, readTagParams, stblPosition, trakPosition, trakSize, trackId, trackCounterOffset, chapterPictureTrackSamples, mediaTimeScale, false);
|
|
if (result > 0) return int32Data; // An error has occured
|
|
}
|
|
|
|
source.BaseStream.Seek(stblPosition, SeekOrigin.Begin);
|
|
|
|
// Samples analysis
|
|
var atomSize = navigateToAtom(source, "stsz");
|
|
if (0 == atomSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "stsz atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte flags
|
|
uint blocByteSizeForAll = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
if (0 == blocByteSizeForAll) // If value other than 0, same size everywhere => CBR
|
|
{
|
|
uint nbSizes = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
uint max = 0;
|
|
uint min = uint.MaxValue;
|
|
|
|
for (int i = 0; i < nbSizes; i++)
|
|
{
|
|
source.Read(data32, 0, 4);
|
|
int32Data = StreamUtils.DecodeBEUInt32(data32);
|
|
min = Math.Min(min, int32Data);
|
|
max = Math.Max(max, int32Data);
|
|
|
|
if (isCurrentTrackFirstChapterTextTrack) chapterTextTrackSamples[i].Size = int32Data;
|
|
if (isCurrentTrackFirstChapterPicturesTrack) chapterPictureTrackSamples[i].Size = int32Data;
|
|
}
|
|
|
|
// VBR detection : if the gap between the smallest and the largest sample size is no more than 1%, we can consider the file is CBR; if not, VBR
|
|
if (isCurrentTrackFirstAudioTrack)
|
|
{
|
|
bitrateTypeID = min * 1.01 < max ? MP4_BITRATE_TYPE_VBR : MP4_BITRATE_TYPE_CBR;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (isCurrentTrackFirstAudioTrack) bitrateTypeID = MP4_BITRATE_TYPE_CBR;
|
|
if (isCurrentTrackFirstChapterTextTrack)
|
|
foreach (var tt in chapterTextTrackSamples) tt.Size = blocByteSizeForAll;
|
|
if (isCurrentTrackFirstChapterPicturesTrack)
|
|
foreach (var pt in chapterPictureTrackSamples) pt.Size = blocByteSizeForAll;
|
|
}
|
|
|
|
// Adjust individual sample offsets using their size for those that are in position > 0 in the same chunk
|
|
if (isCurrentTrackFirstChapterTextTrack)
|
|
{
|
|
uint currentChunkIndex = uint.MaxValue;
|
|
uint cumulatedChunkOffset = 0;
|
|
|
|
foreach (var tt in chapterTextTrackSamples)
|
|
{
|
|
if (tt.ChunkIndex == currentChunkIndex)
|
|
{
|
|
tt.RelativeOffset = cumulatedChunkOffset;
|
|
}
|
|
else
|
|
{
|
|
currentChunkIndex = tt.ChunkIndex;
|
|
cumulatedChunkOffset = 0;
|
|
}
|
|
cumulatedChunkOffset += tt.Size;
|
|
}
|
|
}
|
|
if (isCurrentTrackFirstChapterPicturesTrack)
|
|
{
|
|
uint currentChunkIndex = uint.MaxValue;
|
|
uint cumulatedChunkOffset = 0;
|
|
|
|
foreach (var pt in chapterPictureTrackSamples)
|
|
{
|
|
if (pt.ChunkIndex == currentChunkIndex)
|
|
{
|
|
pt.RelativeOffset = cumulatedChunkOffset;
|
|
}
|
|
else
|
|
{
|
|
currentChunkIndex = pt.ChunkIndex;
|
|
cumulatedChunkOffset = 0;
|
|
}
|
|
cumulatedChunkOffset += pt.Size;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* "Physical" audio chunks are referenced by position (offset) in moov.trak.mdia.minf.stbl.stco / co64
|
|
* => They have to be rewritten if the position (offset) of the 'mdat' atom changes
|
|
*/
|
|
if (readTagParams.PrepareForWriting || isCurrentTrackFirstChapterTextTrack || isCurrentTrackFirstChapterPicturesTrack)
|
|
{
|
|
source.BaseStream.Seek(stblPosition, SeekOrigin.Begin);
|
|
var atomPosition = source.BaseStream.Position;
|
|
byte nbBytes;
|
|
|
|
// Chunk offsets
|
|
if (navigateToAtom(source, "stco") > 0)
|
|
{
|
|
nbBytes = 4;
|
|
}
|
|
else
|
|
{
|
|
source.BaseStream.Seek(atomPosition, SeekOrigin.Begin);
|
|
if (navigateToAtom(source, "co64") > 0)
|
|
{
|
|
nbBytes = 8;
|
|
}
|
|
else
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "neither stco, nor co64 atoms could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
}
|
|
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Version and flags
|
|
var nbChunkOffsets = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
|
|
for (int i = 0; i < nbChunkOffsets; i++)
|
|
{
|
|
object valueObj;
|
|
long valueLong;
|
|
if (4 == nbBytes)
|
|
{
|
|
source.Read(data32, 0, 4);
|
|
valueLong = StreamUtils.DecodeBEUInt32(data32);
|
|
valueObj = (uint)valueLong;
|
|
|
|
}
|
|
else
|
|
{
|
|
source.Read(data64, 0, 8);
|
|
valueLong = StreamUtils.DecodeBEInt64(data64);
|
|
valueObj = valueLong;
|
|
}
|
|
|
|
if (isCurrentTrackFirstChapterTextTrack) // Use the offsets to find position for QT chapter titles
|
|
{
|
|
foreach (var tt in chapterTextTrackSamples)
|
|
{
|
|
if (tt.ChunkIndex == i + 1) tt.ChunkOffset = valueLong;
|
|
}
|
|
}
|
|
else if (isCurrentTrackFirstChapterPicturesTrack)
|
|
{
|
|
// Use the offsets to find position for QT chapter pictures
|
|
foreach (var pt in chapterPictureTrackSamples)
|
|
{
|
|
if (pt.ChunkIndex == i + 1) pt.ChunkOffset = valueLong;
|
|
}
|
|
}
|
|
else if (!isCurrentTrackOtherChapterTrack) // Don't need to save chunks for chapters since they are entirely rewritten
|
|
{
|
|
string zoneName = ZONE_MP4_PHYSICAL_CHUNK + "." + currentTrakIndex + "." + i;
|
|
structureHelper.AddZone(valueLong, 0, zoneName, false, false);
|
|
structureHelper.AddIndex(source.BaseStream.Position - nbBytes, valueObj, false, zoneName);
|
|
}
|
|
} // Chunk offsets
|
|
}
|
|
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
|
|
return trakSize;
|
|
}
|
|
|
|
private uint readQtChapter(
|
|
BinaryReader source,
|
|
ReadTagParams readTagParams,
|
|
long stblPosition,
|
|
long trakPosition,
|
|
uint trakSize,
|
|
int currentTrakIndex,
|
|
long trackCounterOffset,
|
|
IList<MP4Sample> chapterTrackSamples,
|
|
int mediaTimeScale,
|
|
bool isText)
|
|
{
|
|
byte[] data32 = new byte[4];
|
|
|
|
source.BaseStream.Seek(stblPosition, SeekOrigin.Begin);
|
|
if (0 == navigateToAtom(source, "stts"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "stts atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Version and flags
|
|
var int32Data = StreamUtils.DecodeBEUInt32(source.ReadBytes(4)); // Number of table entries
|
|
if (int32Data > 0)
|
|
{
|
|
if (isText)
|
|
qtChapterTextTrackId = currentTrakIndex;
|
|
else
|
|
qtChapterPictureTrackId = currentTrakIndex;
|
|
|
|
// Memorize zone
|
|
if (readTagParams.PrepareForWriting && (Settings.MP4_keepExistingChapters || Settings.MP4_createQuicktimeChapters))
|
|
{
|
|
var zoneName = isText ? ZONE_MP4_QT_CHAP_TXT_TRAK : ZONE_MP4_QT_CHAP_PIC_TRAK;
|
|
structureHelper.AddZone(trakPosition, (int)trakSize, zoneName);
|
|
structureHelper.AddCounter(trackCounterOffset, (1 == currentTrakIndex) ? 2 : 1, zoneName);
|
|
structureHelper.RemoveZone("track." + currentTrakIndex); // Remove previously recorded generic track zone
|
|
structureHelper.RemoveZonesStartingWith(ZONE_MP4_PHYSICAL_CHUNK + "." + currentTrakIndex); // Remove chunks associated with previously recorded generic track zone
|
|
}
|
|
|
|
chapterTrackSamples.Clear();
|
|
|
|
for (int i = 0; i < int32Data; i++)
|
|
{
|
|
source.Read(data32, 0, 4);
|
|
var frameCount = StreamUtils.DecodeBEUInt32(data32);
|
|
source.Read(data32, 0, 4);
|
|
var sampleDuration = StreamUtils.DecodeBEUInt32(data32);
|
|
for (int j = 0; j < frameCount; j++)
|
|
{
|
|
MP4Sample sample = new MP4Sample();
|
|
sample.Duration = sampleDuration * 1.0 / mediaTimeScale;
|
|
chapterTrackSamples.Add(sample);
|
|
}
|
|
}
|
|
}
|
|
|
|
source.BaseStream.Seek(stblPosition, SeekOrigin.Begin);
|
|
if (0 == navigateToAtom(source, "stsc"))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "stsc atom could not be found; aborting read on track " + currentTrakIndex);
|
|
source.BaseStream.Seek(trakPosition + trakSize, SeekOrigin.Begin);
|
|
return trakSize;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Version and flags
|
|
int32Data = StreamUtils.DecodeBEUInt32(source.ReadBytes(4)); // Number of table entries
|
|
|
|
int cumulatedSampleIndex = 0;
|
|
uint chunkIndex = 0;
|
|
uint previousChunkIndex = 0;
|
|
uint previousSamplesPerChunk = 0;
|
|
bool first = true;
|
|
|
|
for (int i = 0; i < int32Data; i++)
|
|
{
|
|
source.Read(data32, 0, 4);
|
|
chunkIndex = StreamUtils.DecodeBEUInt32(data32);
|
|
source.Read(data32, 0, 4);
|
|
var samplesPerChunk = StreamUtils.DecodeBEUInt32(data32);
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Sample description ID
|
|
|
|
if (first)
|
|
{
|
|
first = false;
|
|
}
|
|
else
|
|
{
|
|
for (uint j = previousChunkIndex; j < chunkIndex; j++)
|
|
{
|
|
for (int k = 0; k < previousSamplesPerChunk; k++)
|
|
{
|
|
if (cumulatedSampleIndex < chapterTrackSamples.Count)
|
|
{
|
|
chapterTrackSamples[cumulatedSampleIndex].ChunkIndex = j;
|
|
cumulatedSampleIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
previousChunkIndex = chunkIndex;
|
|
previousSamplesPerChunk = samplesPerChunk;
|
|
}
|
|
|
|
int remainingChunks = (int)Math.Ceiling((chapterTrackSamples.Count - cumulatedSampleIndex) * 1.0 / previousSamplesPerChunk);
|
|
// Fill the rest of the in-memory table with the last pattern
|
|
for (int j = 0; j < remainingChunks; j++)
|
|
{
|
|
for (int k = 0; k < previousSamplesPerChunk; k++)
|
|
{
|
|
if (cumulatedSampleIndex < chapterTrackSamples.Count)
|
|
{
|
|
chapterTrackSamples[cumulatedSampleIndex].ChunkIndex = chunkIndex;
|
|
cumulatedSampleIndex++;
|
|
}
|
|
}
|
|
chunkIndex++;
|
|
}
|
|
|
|
// Look for "trak.edts" atom and save it if it exists
|
|
source.BaseStream.Seek(trakPosition + 8, SeekOrigin.Begin);
|
|
uint edtsSize = navigateToAtom(source, "edts");
|
|
if (edtsSize > 0)
|
|
{
|
|
source.BaseStream.Seek(-8, SeekOrigin.Current);
|
|
if (isText)
|
|
chapterTextTrackEdits = source.ReadBytes((int)edtsSize);
|
|
else
|
|
chapterPictureTrackEdits = source.ReadBytes((int)edtsSize);
|
|
}
|
|
return 0u;
|
|
}
|
|
|
|
private void readUserData(BinaryReader source, ReadTagParams readTagParams, long moovPosition, uint moovSize)
|
|
{
|
|
byte[] data32 = new byte[4];
|
|
byte[] data64 = new byte[8];
|
|
|
|
bool udtaFound = false;
|
|
source.BaseStream.Seek(moovPosition, SeekOrigin.Begin);
|
|
var atomSize = navigateToAtom(source, "udta");
|
|
if (0 == atomSize)
|
|
{
|
|
// If no UDTA has been located in MOOV, look for it into TRAK atoms
|
|
source.BaseStream.Seek(moovPosition, SeekOrigin.Begin);
|
|
atomSize = navigateToAtom(source, "trak");
|
|
while (atomSize > 0)
|
|
{
|
|
var trakPosition = source.BaseStream.Position;
|
|
atomSize = navigateToAtom(source, "udta");
|
|
if (atomSize > 0)
|
|
{
|
|
udtaFound = true;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
source.BaseStream.Seek(trakPosition, SeekOrigin.Begin);
|
|
atomSize = navigateToAtom(source, "trak");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
udtaFound = true;
|
|
}
|
|
|
|
if (!udtaFound)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_INFO, "udta atom could not be found");
|
|
// Create a placeholder to create a new UDTA atom from scratch, located as a direct child of MOOV
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
structureHelper.AddSize(moovPosition - 8 + moovSize, atomSize, ZONE_MP4_NOMETA);
|
|
structureHelper.AddSize(moovPosition - 8 + moovSize, atomSize, ZONE_MP4_ILST);
|
|
structureHelper.AddSize(moovPosition - 8 + moovSize, atomSize, ZONE_MP4_CHPL);
|
|
structureHelper.AddSize(moovPosition - 8 + moovSize, atomSize, ZONE_MP4_XTRA);
|
|
structureHelper.AddZone(moovPosition - 8 + moovSize, 0, ZONE_MP4_NOUDTA);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var udtaPosition = source.BaseStream.Position;
|
|
udtaOffset = udtaPosition;
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
structureHelper.AddSize(source.BaseStream.Position - 8, atomSize, ZONE_MP4_NOMETA);
|
|
structureHelper.AddSize(source.BaseStream.Position - 8, atomSize, ZONE_MP4_ILST);
|
|
structureHelper.AddSize(source.BaseStream.Position - 8, atomSize, ZONE_MP4_CHPL);
|
|
structureHelper.AddSize(source.BaseStream.Position - 8, atomSize, ZONE_MP4_XTRA);
|
|
}
|
|
|
|
// Look for Nero chapters
|
|
var atomPosition = source.BaseStream.Position;
|
|
atomSize = navigateToAtom(source, "chpl");
|
|
if (atomSize > 0 && (Settings.MP4_keepExistingChapters || Settings.MP4_createNeroChapters))
|
|
{
|
|
tagExists = true;
|
|
structureHelper.AddZone(source.BaseStream.Position - 8, (int)atomSize, Array.Empty<byte>(), ZONE_MP4_CHPL);
|
|
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Version and flags
|
|
source.BaseStream.Seek(1, SeekOrigin.Current); // Reserved byte
|
|
source.Read(data32, 0, 4);
|
|
uint neroChapterCount = StreamUtils.DecodeBEUInt32(data32);
|
|
|
|
if (neroChapterCount > 0)
|
|
{
|
|
int qtChapterCount = tagData.Chapters?.Count ?? 0;
|
|
bool isTakeNero = 0 == Settings.MP4_readChaptersFormat && neroChapterCount > qtChapterCount;
|
|
isTakeNero |= 2 == Settings.MP4_readChaptersFormat && neroChapterCount == qtChapterCount;
|
|
isTakeNero |= 3 == Settings.MP4_readChaptersFormat;
|
|
if (isTakeNero)
|
|
{
|
|
tagData.Chapters ??= new List<ChapterInfo>();
|
|
tagData.Chapters.Clear();
|
|
ChapterInfo previousChapter = null;
|
|
|
|
for (int i = 0; i < neroChapterCount; i++)
|
|
{
|
|
var chapter = new ChapterInfo();
|
|
tagData.Chapters.Add(chapter);
|
|
|
|
source.Read(data64, 0, 8);
|
|
chapter.StartTime = (uint)Math.Round(StreamUtils.DecodeBEInt64(data64) / 10000.0);
|
|
if (previousChapter != null) previousChapter.EndTime = chapter.StartTime;
|
|
var stringSize = source.ReadByte();
|
|
chapter.Title = Encoding.UTF8.GetString(source.ReadBytes(stringSize));
|
|
previousChapter = chapter;
|
|
}
|
|
if (previousChapter != null) previousChapter.EndTime = Convert.ToUInt32(Math.Floor(calculatedDurationMs));
|
|
}
|
|
}
|
|
}
|
|
else if (Settings.MP4_createNeroChapters)
|
|
{
|
|
// Allow creating the 'chpl' atom from scratch
|
|
structureHelper.AddZone(atomPosition, 0, ZONE_MP4_CHPL);
|
|
}
|
|
|
|
source.BaseStream.Seek(udtaPosition, SeekOrigin.Begin);
|
|
atomSize = navigateToAtom(source, "meta");
|
|
if (0 == atomSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_INFO, "meta atom could not be found");
|
|
// Allow creating the 'meta' atom from scratch
|
|
structureHelper.AddZone(udtaPosition, 0, ZONE_MP4_NOMETA);
|
|
}
|
|
else
|
|
{
|
|
if (readTagParams.PrepareForWriting) structureHelper.AddSize(source.BaseStream.Position - 8, atomSize, ZONE_MP4_ILST);
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte flags
|
|
if (readTagParams.ReadTag) readTag(source, readTagParams);
|
|
}
|
|
|
|
// Look for Xtra WMA fields (specific fields -e.g. rating- inserted by Windows instead of using standard MP4 fields)
|
|
source.BaseStream.Seek(udtaPosition, SeekOrigin.Begin);
|
|
atomSize = navigateToAtom(source, "Xtra");
|
|
if (atomSize > 7)
|
|
{
|
|
if (readTagParams.PrepareForWriting)
|
|
{
|
|
structureHelper.AddZone(source.BaseStream.Position - 8, (int)atomSize, Array.Empty<byte>(), ZONE_MP4_XTRA);
|
|
}
|
|
if (readTagParams.ReadTag) readXtraTag(source, readTagParams, atomSize - 8);
|
|
}
|
|
}
|
|
|
|
private void readTag(BinaryReader source, ReadTagParams readTagParams)
|
|
{
|
|
var atomPosition = source.BaseStream.Position;
|
|
var atomSize = navigateToAtom(source, "hdlr"); // Metadata handler
|
|
if (0 == atomSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "hdlr atom could not be found; aborting read");
|
|
return;
|
|
}
|
|
long hdlrPosition = source.BaseStream.Position - 8;
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte flags
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // Quicktime type
|
|
string strData = Utils.Latin1Encoding.GetString(source.ReadBytes(4)); // Meta data type
|
|
|
|
if (!strData.Equals("mdir"))
|
|
{
|
|
string errMsg = "ATL does not support ";
|
|
if (strData.Equals("mp7t")) errMsg += "MPEG-7 XML metadata";
|
|
else if (strData.Equals("mp7b")) errMsg += "MPEG-7 binary XML metadata";
|
|
else errMsg = "Unrecognized metadata format";
|
|
|
|
throw new NotSupportedException(errMsg);
|
|
}
|
|
source.BaseStream.Seek(atomSize + hdlrPosition, SeekOrigin.Begin); // Reach the end of the hdlr box
|
|
|
|
long iListSize = navigateToAtom(source, "ilst"); // === Metadata list
|
|
if (0 == iListSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "ilst atom could not be found");
|
|
// TODO handle the case where 'meta' exists, but not 'ilst'
|
|
return;
|
|
}
|
|
structureHelper.AddZone(source.BaseStream.Position - 8, (int)iListSize, ILST_CORE_SIGNATURE, ZONE_MP4_ILST);
|
|
|
|
// Core minimal size
|
|
if (8 == Size) return;
|
|
tagExists = true;
|
|
|
|
StringBuilder atomHeaderBuilder = new StringBuilder();
|
|
// Browse all metadata
|
|
long iListPosition = 0;
|
|
while (iListPosition < iListSize - 8)
|
|
{
|
|
atomHeaderBuilder.Clear();
|
|
atomSize = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
atomHeaderBuilder.Append(Utils.Latin1Encoding.GetString(source.ReadBytes(4)));
|
|
|
|
uint metadataSize;
|
|
if ("----".Equals(atomHeaderBuilder.ToString())) // Custom text metadata
|
|
{
|
|
metadataSize = navigateToAtom(source, "mean"); // "issuer" of the field
|
|
if (0 == metadataSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "mean atom could not be found; aborting read");
|
|
return;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte flags
|
|
string nmeSpace = Utils.Latin1Encoding.GetString(source.ReadBytes((int)metadataSize - 8 - 4));
|
|
|
|
// Only add namespace to the atom name if it's different than the default namespace
|
|
if (!nmeSpace.Equals(DEFAULT_NAMESPACE, StringComparison.OrdinalIgnoreCase))
|
|
atomHeaderBuilder.Append(':').Append(nmeSpace).Append(':');
|
|
else atomHeaderBuilder.Clear();
|
|
|
|
metadataSize = navigateToAtom(source, "name"); // field type
|
|
if (0 == metadataSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "name atom could not be found; aborting read");
|
|
return;
|
|
}
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte flags
|
|
atomHeaderBuilder.Append(Utils.Latin1Encoding.GetString(source.ReadBytes((int)metadataSize - 8 - 4)));
|
|
}
|
|
string atomHeader = atomHeaderBuilder.ToString();
|
|
|
|
// Having a 'data' header here means we're still on the same field, with a 2nd value
|
|
// (e.g. multiple embedded pictures)
|
|
if (!"data".Equals(atomHeader))
|
|
{
|
|
metadataSize = navigateToAtom(source, "data");
|
|
if (0 == metadataSize)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "data atom could not be found; aborting read");
|
|
return;
|
|
}
|
|
atomPosition = source.BaseStream.Position - 8;
|
|
}
|
|
else
|
|
{
|
|
metadataSize = atomSize;
|
|
}
|
|
|
|
// We're only looking for the last byte of the flag
|
|
source.BaseStream.Seek(3, SeekOrigin.Current);
|
|
var dataClass = source.ReadByte();
|
|
|
|
// 4-byte NULL space
|
|
source.BaseStream.Seek(4, SeekOrigin.Current);
|
|
|
|
addFrameClass(atomHeader, dataClass);
|
|
|
|
if (1 == dataClass) // UTF-8 Text
|
|
{
|
|
strData = Encoding.UTF8.GetString(source.ReadBytes((int)metadataSize - 16));
|
|
SetMetaField(atomHeader, strData, readTagParams.ReadAllMetaFrames);
|
|
}
|
|
else if (21 == dataClass) // int8-16-24-32
|
|
{
|
|
int intData;
|
|
uint fieldSize = metadataSize - 16;
|
|
if (fieldSize > 3) intData = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
|
|
else if (fieldSize > 2) intData = StreamUtils.DecodeBEInt24(source.ReadBytes(3));
|
|
else if (fieldSize > 1) intData = StreamUtils.DecodeBEInt16(source.ReadBytes(2));
|
|
else intData = source.ReadByte();
|
|
SetMetaField(atomHeader, intData.ToString(), readTagParams.ReadAllMetaFrames);
|
|
}
|
|
else
|
|
{
|
|
uint uIntData;
|
|
if (22 == dataClass) // uint8-16-24-32
|
|
{
|
|
uint fieldSize = metadataSize - 16;
|
|
if (fieldSize > 3) uIntData = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
|
|
else if (fieldSize > 2) uIntData = StreamUtils.DecodeBEUInt24(source.ReadBytes(3));
|
|
else if (fieldSize > 1) uIntData = StreamUtils.DecodeBEUInt16(source.ReadBytes(2));
|
|
else uIntData = source.ReadByte();
|
|
SetMetaField(atomHeader, uIntData.ToString(), readTagParams.ReadAllMetaFrames);
|
|
}
|
|
else if (13 == dataClass || 14 == dataClass || (0 == dataClass && "covr".Equals(atomHeader))) // Picture
|
|
{
|
|
PictureInfo.PIC_TYPE picType = PictureInfo.PIC_TYPE.Generic; // TODO - to check : this seems to prevent ATL from detecting multiple images from the same type, as for a file with two "Front Cover" images; only one image will be detected
|
|
|
|
uint pictureSize = metadataSize;
|
|
long lastLocation;
|
|
|
|
do
|
|
{
|
|
var picturePosition = takePicturePosition(picType);
|
|
|
|
int dataSize = Math.Max(0, (int)pictureSize - 16);
|
|
if (readTagParams.ReadPictures)
|
|
{
|
|
PictureInfo picInfo = PictureInfo.fromBinaryData(source.BaseStream, dataSize, picType, getImplementedTagType(), dataClass, picturePosition);
|
|
tagData.Pictures.Add(picInfo);
|
|
}
|
|
else
|
|
{
|
|
source.BaseStream.Seek(dataSize, SeekOrigin.Current);
|
|
}
|
|
|
|
// Look for other pictures within 'covr'
|
|
lastLocation = source.BaseStream.Position;
|
|
pictureSize = navigateToAtom(source, "data");
|
|
if (pictureSize > 0)
|
|
{
|
|
// We're only looking for the last byte of the flag
|
|
source.BaseStream.Seek(3, SeekOrigin.Current);
|
|
dataClass = source.ReadByte();
|
|
source.BaseStream.Seek(4, SeekOrigin.Current); // 4-byte NULL space
|
|
|
|
metadataSize += pictureSize;
|
|
}
|
|
} while (pictureSize > 0);
|
|
source.BaseStream.Seek(lastLocation, SeekOrigin.Begin);
|
|
}
|
|
else if (0 == dataClass) // Special cases : gnre, trkn, disk
|
|
{
|
|
if ("trkn".Equals(atomHeader) || "disk".Equals(atomHeader))
|
|
{
|
|
source.BaseStream.Seek(2, SeekOrigin.Current);
|
|
ushort number = StreamUtils.DecodeBEUInt16(source.ReadBytes(2)); // Current track/disc number
|
|
ushort total = StreamUtils.DecodeBEUInt16(source.ReadBytes(2)); // Total number of tracks/discs
|
|
SetMetaField(atomHeader, number.ToString() + "/" + total.ToString(), readTagParams.ReadAllMetaFrames);
|
|
}
|
|
else if ("gnre".Equals(atomHeader)) // ©gen is a text field and doesn't belong here
|
|
{
|
|
uIntData = StreamUtils.DecodeBEUInt16(source.ReadBytes(2));
|
|
|
|
strData = "";
|
|
if (uIntData < ID3v1.MAX_MUSIC_GENRES) strData = ID3v1.MusicGenre[uIntData - 1];
|
|
|
|
SetMetaField(atomHeader, strData, readTagParams.ReadAllMetaFrames);
|
|
}
|
|
// else - Other unhandled cases ?
|
|
}
|
|
}
|
|
// else - Other unhandled cases ?
|
|
|
|
source.BaseStream.Seek(atomPosition + metadataSize, SeekOrigin.Begin);
|
|
iListPosition += atomSize;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read WMA fields located behind the 'Xtra' atom
|
|
* NB : Called _after_ reading standard MP4 tag
|
|
*/
|
|
private void readXtraTag(BinaryReader source, ReadTagParams readTagParams, long atomDataSize)
|
|
{
|
|
IList<KeyValuePair<string, string>> wmaFields = WMAHelper.ReadFields(source.BaseStream, atomDataSize);
|
|
foreach (KeyValuePair<string, string> field in wmaFields)
|
|
setXtraField(field.Key, field.Value, readTagParams.ReadAllMetaFrames || readTagParams.PrepareForWriting);
|
|
}
|
|
|
|
/**
|
|
* Set a WMA field
|
|
*/
|
|
private void setXtraField(string ID, string data, bool readAllMetaFrames)
|
|
{
|
|
// Finds the ATL field identifier
|
|
Field supportedMetaID = WMAHelper.getAtlCodeForFrame(ID);
|
|
|
|
// Hack to format popularity tag with MP4's convention rather than the ASF convention that Xtra uses
|
|
// so that it is parsed properly by MetaDataIO's default mechanisms
|
|
if (Field.RATING == supportedMetaID)
|
|
{
|
|
double? popularity = TrackUtils.DecodePopularity(data, RC_ASF);
|
|
if (popularity.HasValue) data = TrackUtils.EncodePopularity(popularity.Value * 5, ratingConvention) + "";
|
|
else return;
|
|
}
|
|
|
|
// If ID has been mapped with an 'classic' ATL field, store it in the dedicated place...
|
|
if (supportedMetaID != Field.NO_FIELD && !tagData.hasKey(supportedMetaID))
|
|
{
|
|
setMetaField(supportedMetaID, data);
|
|
}
|
|
|
|
if (readAllMetaFrames && ID.Length > 0) // Store it in the additional fields Dictionary
|
|
{
|
|
MetaFieldInfo fieldInfo = new MetaFieldInfo(getImplementedTagType(), ID, data, 0, "", "");
|
|
if (tagData.AdditionalFields.Contains(fieldInfo)) // Prevent duplicates
|
|
{
|
|
tagData.AdditionalFields.Remove(fieldInfo);
|
|
}
|
|
tagData.AdditionalFields.Add(fieldInfo);
|
|
}
|
|
}
|
|
|
|
private static Uuid readUuid(Stream source)
|
|
{
|
|
Uuid result = new Uuid
|
|
{
|
|
size = navigateToAtom(source, "uuid"),
|
|
position = source.Position - 8
|
|
};
|
|
if (result.size >= 16 + 8)
|
|
{
|
|
Span<byte> key = new byte[16];
|
|
source.Read(key);
|
|
// Convert key to hex value
|
|
StringBuilder sbr = new StringBuilder();
|
|
for (int i = 0; i < 16; i++) sbr.Append(key[i].ToString("X2"));
|
|
result.key = sbr.ToString();
|
|
// Read remaining data as UTF-8 string
|
|
long dataSize = result.size - 8 - 16;
|
|
if (dataSize > 0)
|
|
{
|
|
Span<byte> data = new byte[dataSize];
|
|
source.Read(data);
|
|
result.value = Encoding.UTF8.GetString(data);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look for the atom segment starting with the given key, at the current atom level
|
|
/// Returns with Source positioned right after the atom header, on the 1st byte of data
|
|
///
|
|
/// Warning : stream must be positioned at the end of a previous atom before being called
|
|
/// </summary>
|
|
/// <param name="source">Source to read from</param>
|
|
/// <param name="atomKey">Atom key to look for (e.g. "udta")</param>
|
|
/// <returns>If atom found : raw size of the atom (including the already-read 8-byte header);
|
|
/// If atom not found : 0</returns>
|
|
private static uint navigateToAtom(BinaryReader source, string atomKey)
|
|
{
|
|
return navigateToAtom(source.BaseStream, atomKey);
|
|
}
|
|
|
|
private static uint navigateToAtom(Stream source, string atomKey)
|
|
{
|
|
return navigateToAtomSize(source, atomKey).Item1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look for the atom segment starting with the given key, at the current atom level
|
|
/// Returns with Source positioned right after the atom header, on the 1st byte of data
|
|
///
|
|
/// Warning : stream must be positioned at the end of a previous atom before being called
|
|
/// </summary>
|
|
/// <param name="source">Source to read from</param>
|
|
/// <param name="atomKey">Atom key to look for (e.g. "udta")</param>
|
|
/// <returns>
|
|
/// - Item1 : If atom found : raw size of the atom (including the already-read 8 or 16-byte header);
|
|
/// If atom not found : 0
|
|
/// - Item2 : Size of the atom header (may vary if using the 64-bit variant)
|
|
/// </returns>
|
|
private static Tuple<uint, int> navigateToAtomSize(Stream source, string atomKey)
|
|
{
|
|
long atomSize = 0;
|
|
string atomHeader;
|
|
bool first = true;
|
|
int iterations = 0;
|
|
int atomHeaderSize = 8;
|
|
byte[] data = new byte[8];
|
|
|
|
do
|
|
{
|
|
if (!first) source.Seek(atomSize - atomHeaderSize, SeekOrigin.Current);
|
|
atomHeaderSize = 8; // Default variant where size takes up 32-bit
|
|
source.Read(data, 0, 4);
|
|
atomSize = StreamUtils.DecodeBEUInt32(data);
|
|
source.Read(data, 0, 4);
|
|
atomHeader = Utils.Latin1Encoding.GetString(data, 0, 4);
|
|
if (1 == atomSize) // 64-bit size variant
|
|
{
|
|
atomHeaderSize += 8;
|
|
source.Read(data, 0, 8);
|
|
atomSize = StreamUtils.DecodeBEInt64(data);
|
|
}
|
|
|
|
if (first) first = false;
|
|
if (++iterations > 100) return new Tuple<uint, int>(0, atomHeaderSize);
|
|
} while (!atomKey.Equals(atomHeader) && source.Position + atomSize - atomHeaderSize < source.Length);
|
|
|
|
if (source.Position + atomSize - atomHeaderSize > source.Length)
|
|
{
|
|
// atom found, but its declared size goes beyond file size
|
|
if (atomKey.Equals(atomHeader))
|
|
{
|
|
uint actualSize = (uint)(source.Length - source.Position + atomHeaderSize);
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "atom " + atomKey + " has been declared with an incorrect size; using its actual size (" + actualSize + " bytes)");
|
|
return new Tuple<uint, int>(actualSize, atomHeaderSize);
|
|
}
|
|
// atom not found
|
|
return new Tuple<uint, int>(0, atomHeaderSize);
|
|
}
|
|
|
|
var result = atomKey.Equals(atomHeader) ? (uint)atomSize : 0;
|
|
return new Tuple<uint, int>(result, atomHeaderSize);
|
|
}
|
|
|
|
|
|
// Read data from file
|
|
public bool Read(Stream source, AudioDataManager.SizeInfo sizeInfo, ReadTagParams readTagParams)
|
|
{
|
|
this.sizeInfo = sizeInfo;
|
|
|
|
return read(source, readTagParams);
|
|
}
|
|
|
|
protected override bool read(Stream source, ReadTagParams readTagParams)
|
|
{
|
|
if (readTagParams is null) throw new ArgumentNullException(nameof(readTagParams));
|
|
|
|
resetData();
|
|
|
|
BinaryReader reader = new BinaryReader(source);
|
|
|
|
if (recognizeHeaderType(reader)) return readMP4(reader, readTagParams);
|
|
else
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "unknown header type");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void preprocessWrite(TagData dataToWrite)
|
|
{
|
|
// Scan AdditionalData for the need to create the Xtra zone
|
|
foreach (MetaFieldInfo info in dataToWrite.AdditionalFields)
|
|
{
|
|
// Belongs to the XTRA zone + parent UDTA atom has been located => OK
|
|
if (info.NativeFieldCode.StartsWith("WM/", StringComparison.OrdinalIgnoreCase) && udtaOffset > 0)
|
|
{
|
|
// Allow creating the 'xtra' atom / zone from scratch
|
|
structureHelper.AddZone(udtaOffset, 0, ZONE_MP4_XTRA);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Chapter picture-related QA checks specific to MP4/M4A
|
|
if (dataToWrite.Chapters != null)
|
|
{
|
|
long nbPics = dataToWrite.Chapters.LongCount(c => c.Picture != null);
|
|
if (nbPics > 0)
|
|
{
|
|
if (dataToWrite.Chapters[0].StartTime > 0)
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "First chapter start time is > 0:00 - that might cause chapter picture display issues on some players such as VLC.");
|
|
|
|
if (nbPics < dataToWrite.Chapters.Count)
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "Not all chapters have an associated picture - that might cause chapter picture display issues on some players such as VLC.");
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override int write(TagData tag, Stream s, string zone)
|
|
{
|
|
using (BinaryWriter w = new BinaryWriter(s, Encoding.UTF8, true)) return write(tag, w, zone);
|
|
}
|
|
|
|
private int write(TagData tag, BinaryWriter w, string zone)
|
|
{
|
|
int result = 0;
|
|
|
|
if (zone.StartsWith(ZONE_MP4_NOUDTA)) // Create an UDTA atom from scratch
|
|
{
|
|
// Keep position in mind to calculate final size and come back here to write it
|
|
long udtaSizePos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("udta"));
|
|
|
|
result = writeMeta(tag, w);
|
|
|
|
// Record final size of tag into "tag size" fields of header
|
|
long finalTagPos = w.BaseStream.Position;
|
|
|
|
w.BaseStream.Seek(udtaSizePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalTagPos - udtaSizePos)));
|
|
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_NOMETA)) // Create a META atom from scratch
|
|
{
|
|
result = writeMeta(tag, w);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_ILST)) // Edit an existing ILST atom
|
|
{
|
|
// Keep position in mind to calculate final size and come back here to write it
|
|
var tagSizePos = w.BaseStream.Position;
|
|
w.Write(ILST_CORE_SIGNATURE);
|
|
|
|
result = writeFrames(tag, w);
|
|
|
|
// Record final size of tag into "tag size" field of header
|
|
long finalTagPos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(tagSizePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalTagPos - tagSizePos)));
|
|
w.BaseStream.Seek(finalTagPos, SeekOrigin.Begin);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_CHPL)) // Nero chapters
|
|
{
|
|
result = writeNeroChapters(w, Chapters);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_XTRA)) // Extra WMA-like fields written by Windows
|
|
{
|
|
result = writeXtraFrames(tag, w);
|
|
}
|
|
else if (PADDING_ZONE_NAME.Equals(zone)) // Padding
|
|
{
|
|
long paddingSizeToWrite;
|
|
if (tag.PaddingSize > -1) paddingSizeToWrite = tag.PaddingSize;
|
|
else paddingSizeToWrite = TrackUtils.ComputePaddingSize(initialPaddingOffset, initialPaddingSize, structureHelper.GetZone(zone).Offset - structureHelper.getCorrectedOffset(zone));
|
|
|
|
if (paddingSizeToWrite > 0)
|
|
{
|
|
// Placeholder; size is written by FileStructureHelper
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("free"));
|
|
for (int i = 0; i < paddingSizeToWrite - 8; i++) w.Write((byte)0);
|
|
result = 1;
|
|
}
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_QT_CHAP_NOTREF)) // Write a new tref atom for quicktime chapters
|
|
{
|
|
result = writeQTChaptersTref(w, qtChapterTextTrackId, qtChapterPictureTrackId, Chapters);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_QT_CHAP_CHAP)) // Reference to Quicktime chapter track from an audio/video track
|
|
{
|
|
result = writeQTChaptersChap(w, qtChapterTextTrackId, qtChapterPictureTrackId, Chapters);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_QT_CHAP_TXT_TRAK)) // Quicktime chapter text track
|
|
{
|
|
if (zone.Equals(ZONE_MP4_QT_CHAP_TXT_TRAK)) // Text track ATL suppors
|
|
result = writeQTChaptersTrack(w, qtChapterTextTrackId, Chapters, globalTimeScale, Convert.ToUInt32(calculatedDurationMs), true);
|
|
else return 1; // Other text track ATL doesn't support; needs to appear active
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_QT_CHAP_PIC_TRAK)) // Quicktime chapter picture track
|
|
{
|
|
result = writeQTChaptersTrack(w, qtChapterPictureTrackId, Chapters, globalTimeScale, Convert.ToUInt32(calculatedDurationMs), false);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_QT_CHAP_MDAT)) // Quicktime chapter data (text and picture data)
|
|
{
|
|
result = writeQTChaptersData(w, Chapters);
|
|
}
|
|
else if (zone.StartsWith("uuid.")) // Existing UUID atoms
|
|
{
|
|
result = writeUuidFrame(tag, zone[5..], w);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_ADDITIONAL_UUIDS)) // Extra UUID atoms
|
|
{
|
|
result = writeUuidFrames(tag, w);
|
|
}
|
|
else if (zone.StartsWith(ZONE_MP4_PHYSICAL_CHUNK)) // Audio chunks
|
|
{
|
|
result = 1; // Needs to appear active in case their headers need to be rewritten (e.g. chunk enlarged somewhere -> all physical chunks are X bytes ahead of their initial position)
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private int writeMeta(TagData tag, BinaryWriter w)
|
|
{
|
|
// Keep position in mind to calculate final size and come back here to write it
|
|
long metaSizePos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("meta"));
|
|
w.Write(0); // version and flags
|
|
|
|
// Handler
|
|
long hdlrPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("hdlr"));
|
|
w.Write(0); // version and flags
|
|
w.Write(0); // quicktime type
|
|
w.Write(Utils.Latin1Encoding.GetBytes("mdir")); // quicktime subtype = "APPLE meta data iTunes reader"
|
|
w.Write(Utils.Latin1Encoding.GetBytes("appl")); // manufacturer
|
|
w.Write(0); // component flags
|
|
w.Write(0); // component flags mask
|
|
w.Write(Utils.Latin1Encoding.GetBytes("Metadata (ilst) handler\0")); // component name
|
|
|
|
long ilstSizePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(hdlrPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(ilstSizePos - hdlrPos)));
|
|
|
|
w.BaseStream.Seek(ilstSizePos, SeekOrigin.Begin);
|
|
w.Write(ILST_CORE_SIGNATURE);
|
|
|
|
int result = writeFrames(tag, w);
|
|
|
|
// Record final size of tag into "tag size" fields of header
|
|
long finalTagPos = w.BaseStream.Position;
|
|
|
|
w.BaseStream.Seek(metaSizePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalTagPos - metaSizePos)));
|
|
|
|
w.BaseStream.Seek(ilstSizePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalTagPos - ilstSizePos)));
|
|
|
|
w.BaseStream.Seek(finalTagPos, SeekOrigin.Begin);
|
|
|
|
return result;
|
|
}
|
|
|
|
private int writeFrames(TagData tag, BinaryWriter w)
|
|
{
|
|
int counter = 0;
|
|
// Keep these in memory to prevent setting them twice using AdditionalFields
|
|
var writtenFieldCodes = new HashSet<string>();
|
|
|
|
IDictionary<Field, string> map = tag.ToMap();
|
|
|
|
// Supported textual fields
|
|
foreach (Field frameType in map.Keys)
|
|
{
|
|
foreach (string s in frameMapping_mp4.Keys)
|
|
{
|
|
if (frameType == frameMapping_mp4[s])
|
|
{
|
|
if (map[frameType].Length > 0) // No frame with empty value
|
|
{
|
|
string value = formatBeforeWriting(frameType, tag, map);
|
|
|
|
writeTextFrame(w, s, value);
|
|
writtenFieldCodes.Add(s.ToUpper());
|
|
counter++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Other textual fields
|
|
foreach (MetaFieldInfo fieldInfo in tag.AdditionalFields)
|
|
{
|
|
if (
|
|
(fieldInfo.TagType.Equals(MetaDataIOFactory.TagType.ANY) || fieldInfo.TagType.Equals(getImplementedTagType()))
|
|
&& !fieldInfo.MarkedForDeletion
|
|
&& !fieldInfo.NativeFieldCode.StartsWith("uuid.")
|
|
&& !fieldInfo.NativeFieldCode.StartsWith("xmp.")
|
|
&& !writtenFieldCodes.Contains(fieldInfo.NativeFieldCode.ToUpper())
|
|
)
|
|
{
|
|
writeTextFrame(w, fieldInfo.NativeFieldCode, FormatBeforeWriting(fieldInfo.Value));
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
// Picture fields
|
|
bool firstPic = true;
|
|
bool hasPic = false;
|
|
long picHeaderPos = 0;
|
|
foreach (PictureInfo picInfo in tag.Pictures)
|
|
{
|
|
// Picture has either to be supported, or to come from the right tag standard
|
|
var doWritePicture = !picInfo.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported);
|
|
if (!doWritePicture) doWritePicture = getImplementedTagType() == picInfo.TagType;
|
|
// It also has not to be marked for deletion
|
|
doWritePicture = doWritePicture && !picInfo.MarkedForDeletion;
|
|
|
|
if (!doWritePicture) continue;
|
|
|
|
hasPic = true;
|
|
if (firstPic)
|
|
{
|
|
// If multiples pictures are embedded, the 'covr' atom is not repeated; the 'data' atom is
|
|
picHeaderPos = w.BaseStream.Position;
|
|
w.Write(0); // Frame size placeholder to be rewritten in a few lines
|
|
w.Write(Utils.Latin1Encoding.GetBytes("covr"));
|
|
firstPic = false;
|
|
}
|
|
|
|
writePictureFrame(w, picInfo.PictureData, picInfo.NativeFormat);
|
|
counter++;
|
|
}
|
|
if (hasPic)
|
|
{
|
|
long finalPos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(picHeaderPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalPos - picHeaderPos)));
|
|
w.BaseStream.Seek(finalPos, SeekOrigin.Begin);
|
|
}
|
|
|
|
return counter;
|
|
}
|
|
|
|
private void writeTextFrame(BinaryWriter writer, string frameCode, string text)
|
|
{
|
|
const int frameFlags = 0;
|
|
|
|
// == METADATA HEADER ==
|
|
var frameSizePos1 = writer.BaseStream.Position;
|
|
writer.Write(0); // Frame size placeholder to be rewritten in a few lines
|
|
|
|
if (!frameCode.StartsWith("WM/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Non-Microsoft custom metadata
|
|
if (frameCode.Length != FieldCodeFixedLength)
|
|
{
|
|
string[] frameCodeComponents = frameCode.Split(':');
|
|
string nmespace = DEFAULT_NAMESPACE;
|
|
if (2 == frameCodeComponents.Length && !frameCodeComponents[0].StartsWith("--")) nmespace = frameCodeComponents[0];
|
|
string fieldCode = frameCodeComponents[^1];
|
|
|
|
writer.Write(Utils.Latin1Encoding.GetBytes("----"));
|
|
|
|
writer.Write(StreamUtils.EncodeBEInt32(nmespace.Length + 4 + 4 + 4));
|
|
writer.Write(Utils.Latin1Encoding.GetBytes("mean"));
|
|
writer.Write(frameFlags);
|
|
writer.Write(Utils.Latin1Encoding.GetBytes(nmespace));
|
|
|
|
writer.Write(StreamUtils.EncodeBEInt32(fieldCode.Length + 4 + 4 + 4));
|
|
writer.Write(Utils.Latin1Encoding.GetBytes("name"));
|
|
writer.Write(frameFlags);
|
|
writer.Write(Utils.Latin1Encoding.GetBytes(fieldCode));
|
|
}
|
|
else // Standard-looking metadata
|
|
{
|
|
writer.Write(Utils.Latin1Encoding.GetBytes(frameCode));
|
|
}
|
|
}
|
|
|
|
// == METADATA VALUE ==
|
|
var frameSizePos2 = writer.BaseStream.Position;
|
|
writer.Write(0); // Frame size placeholder to be rewritten in a few lines
|
|
writer.Write(Utils.Latin1Encoding.GetBytes("data"));
|
|
|
|
int frameClass = 1;
|
|
if (frameClasses_mp4.TryGetValue(frameCode, out var value1)) frameClass = value1;
|
|
|
|
writer.Write(StreamUtils.EncodeBEInt32(frameClass));
|
|
writer.Write(frameFlags);
|
|
|
|
if (0 == frameClass) // Special cases : gnre, trkn, disk
|
|
{
|
|
byte[] int16data;
|
|
if (frameCode.Equals("trkn") || frameCode.Equals("disk"))
|
|
{
|
|
int16data = new byte[2] { 0, 0 };
|
|
writer.Write(int16data);
|
|
int16data = StreamUtils.EncodeBEUInt16(TrackUtils.ExtractTrackNumber(text));
|
|
writer.Write(int16data);
|
|
int16data = StreamUtils.EncodeBEUInt16(TrackUtils.ExtractTrackTotal(text));
|
|
writer.Write(int16data);
|
|
if (frameCode.Equals("trkn")) writer.Write(int16data); // trkn field always has two more bytes than disk field....
|
|
}
|
|
else if (frameCode.Equals("gnre"))
|
|
{
|
|
int16data = StreamUtils.EncodeBEUInt16(Convert.ToUInt16(text));
|
|
writer.Write(int16data);
|
|
}
|
|
}
|
|
else if (1 == frameClass) // UTF-8 text
|
|
{
|
|
writer.Write(Encoding.UTF8.GetBytes(text));
|
|
}
|
|
else if (21 == frameClass) // int8-16-24-32, depending on the value
|
|
{
|
|
if (!Utils.IsNumeric(text, true))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "value " + text + " could not be converted to integer; ignoring");
|
|
writer.Write(0);
|
|
}
|
|
else
|
|
{
|
|
int value = Convert.ToInt32(text);
|
|
if (value > short.MaxValue || value < short.MinValue) writer.Write(StreamUtils.EncodeBEInt32(value));
|
|
// use int32 instead of int24 because Convert.ToInt24 doesn't exist
|
|
else if (value > 127 || value < -127) writer.Write(StreamUtils.EncodeBEInt16(Convert.ToInt16(text)));
|
|
else writer.Write(Convert.ToByte(text));
|
|
}
|
|
}
|
|
else if (22 == frameClass) // uint8-16-24-32, depending on the value
|
|
{
|
|
if (!Utils.IsNumeric(text, true, false))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "value " + text + " could not be converted to unsigned integer; ignoring");
|
|
writer.Write(0);
|
|
}
|
|
else
|
|
{
|
|
uint value = Convert.ToUInt32(text);
|
|
if (value > 0xffff) writer.Write(StreamUtils.EncodeBEUInt32(value));
|
|
// use int32 instead of int24 because Convert.ToUInt24 doesn't exist
|
|
else if (value > 0xff) writer.Write(StreamUtils.EncodeBEUInt16(Convert.ToUInt16(text)));
|
|
else writer.Write(Convert.ToByte(text));
|
|
}
|
|
}
|
|
|
|
|
|
// Go back to frame size locations to write their actual size
|
|
var finalFramePos = writer.BaseStream.Position;
|
|
writer.BaseStream.Seek(frameSizePos1, SeekOrigin.Begin);
|
|
writer.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalFramePos - frameSizePos1)));
|
|
writer.BaseStream.Seek(frameSizePos2, SeekOrigin.Begin);
|
|
writer.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalFramePos - frameSizePos2)));
|
|
writer.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
}
|
|
|
|
private static void writePictureFrame(BinaryWriter writer, byte[] pictureData, ImageFormat picFormat)
|
|
{
|
|
const int frameFlags = 0;
|
|
|
|
var frameSizePos2 = writer.BaseStream.Position;
|
|
writer.Write(0); // Frame size placeholder to be rewritten in a few lines
|
|
writer.Write("data".ToCharArray());
|
|
|
|
int frameClass;
|
|
if (picFormat.Equals(ImageFormat.Jpeg)) frameClass = 13;
|
|
else if (picFormat.Equals(ImageFormat.Png)) frameClass = 14;
|
|
else frameClass = 0;
|
|
|
|
writer.Write(StreamUtils.EncodeBEInt32(frameClass));
|
|
writer.Write(frameFlags);
|
|
|
|
writer.Write(pictureData);
|
|
|
|
// Go back to frame size locations to write their actual size
|
|
var finalFramePos = writer.BaseStream.Position;
|
|
writer.BaseStream.Seek(frameSizePos2, SeekOrigin.Begin);
|
|
writer.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(finalFramePos - frameSizePos2)));
|
|
writer.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
}
|
|
|
|
private int writeXtraFrames(TagData tag, BinaryWriter w)
|
|
{
|
|
IEnumerable<MetaFieldInfo> xtraTags = tag.AdditionalFields.Where(fi => (fi.TagType.Equals(MetaDataIOFactory.TagType.ANY) || fi.TagType.Equals(getImplementedTagType())) && !fi.MarkedForDeletion && fi.NativeFieldCode.ToLower().StartsWith("wm/", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (!xtraTags.Any()) return 0;
|
|
|
|
// Start writing the atom
|
|
var frameSizePos = w.BaseStream.Position;
|
|
|
|
w.Write(0); // To be rewritten at the end of the method
|
|
w.Write(Utils.Latin1Encoding.GetBytes("Xtra"));
|
|
|
|
// Write all fields
|
|
foreach (MetaFieldInfo fieldInfo in xtraTags)
|
|
{
|
|
// Write the value of the "master" field contained in TagData
|
|
string value = WMAHelper.getValueFromTagData(fieldInfo.NativeFieldCode, tag);
|
|
// if no "master" field is set, write the extra field's own value
|
|
if (string.IsNullOrEmpty(value)) value = fieldInfo.Value;
|
|
|
|
bool isNumeric = false;
|
|
// Hack to format popularity tag with the ASF convention rather than the convention that MP4 uses
|
|
// so that it is parsed properly by Windows
|
|
if ("wm/shareduserrating" == fieldInfo.NativeFieldCode.ToLower())
|
|
{
|
|
double popularity;
|
|
if (double.TryParse(value, out popularity))
|
|
{
|
|
value = TrackUtils.EncodePopularity(popularity * 5, MetaDataIO.RC_ASF) + "";
|
|
isNumeric = true;
|
|
}
|
|
else continue;
|
|
}
|
|
|
|
WMAHelper.WriteField(w, fieldInfo.NativeFieldCode, value, isNumeric);
|
|
}
|
|
|
|
// Go back to frame size locations to write their actual size
|
|
var finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(frameSizePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - frameSizePos)));
|
|
|
|
return xtraTags.Count();
|
|
}
|
|
|
|
private static int writeNeroChapters(BinaryWriter w, IList<ChapterInfo> chapters)
|
|
{
|
|
if (null == chapters || 0 == chapters.Count) return 0;
|
|
|
|
int result = 0;
|
|
|
|
result = chapters.Count;
|
|
|
|
var frameSizePos = w.BaseStream.Position;
|
|
w.Write(0); // To be rewritten at the end of the method
|
|
w.Write(Utils.Latin1Encoding.GetBytes("chpl"));
|
|
|
|
w.Write(new byte[] { 1, 0, 0, 0, 0 }); // Version, flags and reserved byte
|
|
|
|
int maxCount = Settings.MP4_capNeroChapters ? Math.Min(chapters.Count, 255) : chapters.Count;
|
|
w.Write(StreamUtils.EncodeBEInt32(maxCount));
|
|
|
|
for (int i = 0; i < maxCount; i++)
|
|
{
|
|
ChapterInfo chapter = chapters[i];
|
|
w.Write(StreamUtils.EncodeBEUInt64((ulong)chapter.StartTime * 10000));
|
|
var strData = Encoding.UTF8.GetBytes(chapter.Title);
|
|
var strDataLength = (byte)Math.Min(255, strData.Length);
|
|
w.Write(strDataLength);
|
|
w.Write(strData, 0, strDataLength);
|
|
}
|
|
|
|
// Go back to frame size locations to write their actual size
|
|
var finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(frameSizePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - frameSizePos)));
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int writeQTChaptersTref(BinaryWriter w, int qtChapterTextTrackNum, int qtChapterPictureTrackNum, IList<ChapterInfo> chapters)
|
|
{
|
|
if (null == chapters || 0 == chapters.Count) return 0;
|
|
|
|
long trefPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("tref"));
|
|
|
|
writeQTChaptersChap(w, qtChapterTextTrackNum, qtChapterPictureTrackNum, chapters);
|
|
|
|
long finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(trefPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - trefPos)));
|
|
|
|
return 1;
|
|
}
|
|
|
|
private static int writeQTChaptersChap(BinaryWriter w, int qtChapterTextTrackNum, int qtChapterPictureTrackNum, ICollection<ChapterInfo> chapters)
|
|
{
|
|
if (null == chapters || 0 == chapters.Count) return 0;
|
|
|
|
long chapPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("chap"));
|
|
|
|
if (qtChapterTextTrackNum > 0)
|
|
w.Write(StreamUtils.EncodeBEInt32(qtChapterTextTrackNum));
|
|
|
|
int nbActualChapterImages = chapters.Count(ch => ch.Picture != null && ch.Picture.PictureData.Length > 0);
|
|
if (qtChapterPictureTrackNum > 0 && nbActualChapterImages > 0)
|
|
w.Write(StreamUtils.EncodeBEInt32(qtChapterPictureTrackNum)); // As many pictures as there are chapters
|
|
|
|
long finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(chapPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - chapPos)));
|
|
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
|
|
return 1;
|
|
}
|
|
|
|
private static int writeQTChaptersData(BinaryWriter w, ICollection<ChapterInfo> chapters)
|
|
{
|
|
if (null == chapters || 0 == chapters.Count) return 0;
|
|
|
|
foreach (ChapterInfo chapter in chapters)
|
|
{
|
|
byte[] titleBytes = Encoding.UTF8.GetBytes(chapter.Title);
|
|
w.Write(StreamUtils.EncodeBEInt16((short)titleBytes.Length));
|
|
w.Write(titleBytes);
|
|
// Magic sequence (always the same)
|
|
w.Write(StreamUtils.EncodeBEInt32(12));
|
|
w.Write(Utils.Latin1Encoding.GetBytes("encd"));
|
|
w.Write(StreamUtils.EncodeBEInt32(256));
|
|
}
|
|
|
|
foreach (var chapter in chapters)
|
|
{
|
|
if (chapter.Picture != null) w.Write(chapter.Picture.PictureData);
|
|
else w.Write(Properties.Resources._1px_black);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
private int writeQTChaptersTrack(BinaryWriter w, int trackNum, IList<ChapterInfo> chapters, uint globalTimeScale, uint trackDurationMs, bool isText)
|
|
{
|
|
long trackTimescale = trackTimescales[trackNum];
|
|
|
|
if (null == chapters || 0 == chapters.Count) return 0;
|
|
IList<ChapterInfo> workingChapters = chapters;
|
|
if (0 == workingChapters.Count) return 0;
|
|
|
|
// Find largest dimensions and color depth among all chapter pictures
|
|
short maxWidth = 0;
|
|
short maxHeight = 0;
|
|
int maxDepth = 0;
|
|
if (!isText)
|
|
{
|
|
foreach (ChapterInfo chapter in workingChapters)
|
|
{
|
|
byte[] pictureData = chapter.Picture != null
|
|
? chapter.Picture.PictureData
|
|
: Properties.Resources._1px_black;
|
|
ImageProperties props = ImageUtils.GetImageProperties(pictureData);
|
|
maxWidth = (short)Math.Min(Math.Max(props.Width, maxWidth), short.MaxValue);
|
|
maxHeight = (short)Math.Min(Math.Max(props.Height, maxHeight), short.MaxValue);
|
|
maxDepth = Math.Max(props.ColorDepth, maxDepth);
|
|
}
|
|
}
|
|
|
|
// TRACK
|
|
long trakPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("trak"));
|
|
|
|
// TRACK HEADER BEGIN
|
|
w.Write(StreamUtils.EncodeBEInt32(92)); // Standard size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("tkhd"));
|
|
|
|
w.Write((byte)0); // Version
|
|
w.Write((short)0); // Flags(bytes 2,3)
|
|
w.Write((byte)7); // Flags(byte 1) --> TrackEnabled = 1 ; TrackInMovie = 2 ; TrackInPreview = 4; TrackInPoster = 8
|
|
|
|
w.Write(StreamUtils.EncodeBEUInt32(getMacDateNow())); // Creation date
|
|
w.Write(StreamUtils.EncodeBEUInt32(getMacDateNow())); // Modification date
|
|
|
|
w.Write(StreamUtils.EncodeBEInt32(trackNum)); // Track number
|
|
|
|
w.Write(0); // Reserved
|
|
|
|
w.Write(StreamUtils.EncodeBEUInt32(trackDurationMs / 1000 * globalTimeScale)); // Duration (sec)
|
|
|
|
w.Write((long)0); // Reserved
|
|
w.Write(StreamUtils.EncodeBEInt16((short)(isText ? 2 : 1))); // Layer
|
|
w.Write((short)0); // Alternate group
|
|
w.Write((short)0); // Volume
|
|
w.Write((short)0); // Reserved
|
|
|
|
// Matrix (keep values of sample file)
|
|
w.Write(new byte[] { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0 });
|
|
|
|
w.Write(encodeBEFixedPoint32(maxWidth, 0)); // Width
|
|
w.Write(encodeBEFixedPoint32(maxHeight, 0)); // Height
|
|
|
|
// TRACK HEADER END
|
|
|
|
|
|
// EDITS BEGIN (optional)
|
|
if (isText && chapterTextTrackEdits != null) w.Write(chapterTextTrackEdits);
|
|
if (!isText && chapterPictureTrackEdits != null) w.Write(chapterPictureTrackEdits);
|
|
// EDITS END (optional)
|
|
|
|
|
|
// MEDIA BEGIN
|
|
long mdiaPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("mdia"));
|
|
|
|
// MEDIA HEADER
|
|
w.Write(StreamUtils.EncodeBEInt32(32)); // Standard size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("mdhd"));
|
|
w.Write(0); // Version and flags
|
|
|
|
w.Write(StreamUtils.EncodeBEUInt32(getMacDateNow())); // Creation date
|
|
w.Write(StreamUtils.EncodeBEUInt32(getMacDateNow())); // Modification date
|
|
|
|
w.Write(StreamUtils.EncodeBEUInt32((uint)trackTimescale)); // Track timescale
|
|
|
|
w.Write(StreamUtils.EncodeBEUInt32((uint)(trackDurationMs / 1000 * trackTimescale))); // Duration (sec)
|
|
|
|
w.Write(new byte[] { 0x55, 0xc4 }); // Code for English - TODO : make that dynamic
|
|
|
|
w.Write((short)0); // Quicktime quality
|
|
// MEDIA HEADER END
|
|
|
|
// MEDIA HANDLER
|
|
long hdlrPos = w.BaseStream.Position;
|
|
w.Write(0); // Temp; will be rewritten later
|
|
w.Write(Utils.Latin1Encoding.GetBytes("hdlr"));
|
|
w.Write(0); // Version and flags
|
|
|
|
w.Write(0); // Quicktime type
|
|
if (isText)
|
|
w.Write(Utils.Latin1Encoding.GetBytes("text")); // Subtype
|
|
else
|
|
w.Write(Utils.Latin1Encoding.GetBytes("vide")); // Subtype
|
|
w.Write(0); // Reserved
|
|
w.Write(0); // Reserved
|
|
w.Write(0); // Reserved
|
|
w.Write(Utils.Latin1Encoding.GetBytes(isText ? "Chapter titles\0" : "Chapter pictures\0")); // component name
|
|
|
|
long minfPos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(hdlrPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEUInt32(Convert.ToUInt32(minfPos - hdlrPos)));
|
|
// MEDIA HANDLER END
|
|
|
|
// MEDIA INFORMATION
|
|
w.BaseStream.Seek(minfPos, SeekOrigin.Begin);
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("minf"));
|
|
|
|
// BASE MEDIA INFORMATION HEADER
|
|
if (isText)
|
|
{
|
|
w.Write(StreamUtils.EncodeBEInt32(76)); // Standard size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("gmhd"));
|
|
|
|
w.Write(StreamUtils.EncodeBEInt32(24)); // Standard size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("gmin"));
|
|
w.Write(0); // Version and flags
|
|
w.Write(StreamUtils.EncodeBEInt16((short)64)); // Graphics mode
|
|
w.Write(new byte[] { 0x80, 0 }); // Opcolor 1
|
|
w.Write(new byte[] { 0x80, 0 }); // Opcolor 2
|
|
w.Write(new byte[] { 0x80, 0 }); // Opcolor 3
|
|
w.Write(0); // Balance + reserved
|
|
|
|
w.Write(StreamUtils.EncodeBEInt32(44)); // Standard size
|
|
if (isText)
|
|
w.Write(Utils.Latin1Encoding.GetBytes("text")); // Subtype
|
|
else
|
|
w.Write(Utils.Latin1Encoding.GetBytes("vide")); // Subtype
|
|
// Matrix (keep values of sample file)
|
|
w.Write(new byte[] { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0 });
|
|
}
|
|
else
|
|
{
|
|
w.Write(StreamUtils.EncodeBEInt32(20)); // Standard size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("vmhd"));
|
|
|
|
w.Write((byte)0); // Version
|
|
w.Write((short)0); // Flags, bytes 2 and 3
|
|
w.Write((byte)1); // Flags, byte 1
|
|
w.Write((short)0); // Graphics mode
|
|
w.Write((short)0); // OpColor R
|
|
w.Write((short)0); // OpColor G
|
|
w.Write((short)0); // OpColor B
|
|
}
|
|
// END BASE MEDIA INFORMATION HEADER
|
|
|
|
|
|
// DATA INFORMATION
|
|
w.Write(StreamUtils.EncodeBEInt32(36)); // Predetermined size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("dinf"));
|
|
|
|
// DATA REFERENCE
|
|
w.Write(StreamUtils.EncodeBEInt32(28)); // Predetermined size
|
|
w.Write(Utils.Latin1Encoding.GetBytes("dref"));
|
|
w.Write(0); // Version and flags
|
|
w.Write(StreamUtils.EncodeBEInt32(1)); // Number of refs
|
|
w.Write(StreamUtils.EncodeBEInt32(12)); // Entry length
|
|
w.Write(Utils.Latin1Encoding.GetBytes("url ")); // Entry code
|
|
w.Write(StreamUtils.EncodeBEInt32(1)); // Entry data
|
|
|
|
// SAMPLE TABLE BEGIN
|
|
long stblPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("stbl"));
|
|
|
|
// SAMPLE DESCRIPTION
|
|
long stsdPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("stsd"));
|
|
w.Write(0); // Version and flags
|
|
w.Write(StreamUtils.EncodeBEInt32(1)); // Number of descriptions
|
|
|
|
long subtypePos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
if (isText)
|
|
{
|
|
// General structure
|
|
w.Write(Utils.Latin1Encoding.GetBytes("text")); // Subtype ('text' for text; 'tx3g' for subtitles)
|
|
w.Write(0); // Reserved
|
|
w.Write((short)0); // Reserved
|
|
w.Write(StreamUtils.EncodeBEInt16(1)); // Data reference index (TODO - is that dynamic ?)
|
|
|
|
// Text sample properties
|
|
w.Write(StreamUtils.EncodeBEInt32(1)); // Display flags
|
|
w.Write(StreamUtils.EncodeBEInt32(1)); // Text justification
|
|
w.Write(0); // Text background color
|
|
w.Write((short)0); // Text background color
|
|
w.Write((long)0); // Default text box
|
|
w.Write((long)0); // Reserved
|
|
w.Write((short)0); // Font number
|
|
w.Write((short)0); // Font face
|
|
w.Write((byte)0); // Reserved
|
|
w.Write((short)0); // Reserved
|
|
w.Write(0); // Foreground color
|
|
w.Write((short)0); // Foreground color
|
|
// w.Write((byte)0); // No text
|
|
}
|
|
else
|
|
{
|
|
// General structure
|
|
// TODO PNG support
|
|
w.Write(Utils.Latin1Encoding.GetBytes("jpeg")); // Subtype
|
|
w.Write(0); // Reserved
|
|
w.Write((short)0); // Reserved
|
|
w.Write(StreamUtils.EncodeBEInt16(1)); // Data reference index (TODO - is that dynamic ?)
|
|
|
|
// Video sample properties
|
|
w.Write((short)0); // Version
|
|
w.Write((short)0); // Revision level
|
|
w.Write(0); // Vendor
|
|
w.Write(0); // Temporal quality
|
|
w.Write(0); // Spatial quality
|
|
|
|
w.Write(StreamUtils.EncodeBEInt16(maxWidth)); // Width
|
|
w.Write(StreamUtils.EncodeBEInt16(maxHeight)); // Height
|
|
|
|
w.Write(new byte[] { 0, 0x48, 0, 0 }); // Horizontal resolution (32 bits fixed-point; reusing sample file data for now)
|
|
w.Write(new byte[] { 0, 0x48, 0, 0 }); // Vertical resolution (32 bits fixed-point; reusing sample file data for now)
|
|
w.Write(0); // Data size
|
|
w.Write(StreamUtils.EncodeBEInt16(1)); // Frame count
|
|
//w.Write(Utils.Latin1Encoding.GetBytes("jpeg")); // Compressor name
|
|
w.Write(0); // Compressor name
|
|
/*
|
|
w.Write(StreamUtils.EncodeBEInt16((short)Math.Min(maxDepth, short.MaxValue))); // Color depth
|
|
w.Write(StreamUtils.EncodeBEInt16(-1)); // Color table
|
|
*/
|
|
// Color depth and table (keep values of sample file)
|
|
w.Write(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x18, 0xFF, 0xFF });
|
|
}
|
|
long finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(stsdPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - stsdPos)));
|
|
w.BaseStream.Seek(subtypePos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - subtypePos)));
|
|
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
|
|
// TIME TO SAMPLE START
|
|
long sttsPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("stts"));
|
|
w.Write(0); // Version and flags
|
|
w.Write(StreamUtils.EncodeBEInt32(workingChapters.Count));
|
|
foreach (ChapterInfo chapter in workingChapters)
|
|
{
|
|
w.Write(StreamUtils.EncodeBEUInt32(1));
|
|
w.Write(StreamUtils.EncodeBEUInt32((uint)Math.Ceiling((chapter.EndTime - chapter.StartTime) * trackTimescale / 1000.0)));
|
|
}
|
|
finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(sttsPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - sttsPos)));
|
|
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
// TIME TO SAMPLE END
|
|
|
|
// SAMPLE <-> CHUNK START
|
|
long stscPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("stsc"));
|
|
w.Write(0); // Version and flags
|
|
w.Write(StreamUtils.EncodeBEInt32(1));
|
|
// Attach all samples to 1st chunk
|
|
w.Write(StreamUtils.EncodeBEInt32(1));
|
|
w.Write(StreamUtils.EncodeBEInt32(workingChapters.Count));
|
|
w.Write(StreamUtils.EncodeBEInt32(1));
|
|
finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(stscPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - stscPos)));
|
|
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
// SAMPLE <-> CHUNK END
|
|
|
|
// SAMPLE SIZE START
|
|
long stszPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("stsz"));
|
|
w.Write(0); // Version and flags
|
|
w.Write(0); // Different block sizes
|
|
w.Write(StreamUtils.EncodeBEInt32(workingChapters.Count));
|
|
long totalTrackTxtSize = 0;
|
|
foreach (ChapterInfo chapter in chapters)
|
|
{
|
|
long trackTxtSize = 2 + Encoding.UTF8.GetBytes(chapter.Title).Length + 12;
|
|
totalTrackTxtSize += trackTxtSize;
|
|
if (isText) w.Write(StreamUtils.EncodeBEUInt32((uint)trackTxtSize));
|
|
}
|
|
if (!isText)
|
|
{
|
|
foreach (ChapterInfo chapter in workingChapters)
|
|
{
|
|
byte[] pictureData = chapter.Picture != null
|
|
? chapter.Picture.PictureData
|
|
: Properties.Resources._1px_black;
|
|
w.Write(StreamUtils.EncodeBEUInt32((uint)pictureData.Length));
|
|
}
|
|
}
|
|
finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(stszPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - stszPos)));
|
|
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
// SAMPLE SIZE END
|
|
|
|
// CHUNK OFFSET START
|
|
long stcoPos = w.BaseStream.Position;
|
|
w.Write(0);
|
|
w.Write(Utils.Latin1Encoding.GetBytes("stco"));
|
|
w.Write(0); // Version and flags
|
|
|
|
w.Write(StreamUtils.EncodeBEInt32(1));
|
|
|
|
// Calculate chunk offset and feed it to FileStructureHelper as a header to the MDAT zone
|
|
// - Physically located in the TRAK zone
|
|
// - Child of the TRAK zone (i.e. won't be useful to process if the TRAK zone is deleted)
|
|
// NB : Only works when QT track is located _before_ QT mdat
|
|
string zoneId = isText ? ZONE_MP4_QT_CHAP_TXT_TRAK : ZONE_MP4_QT_CHAP_PIC_TRAK;
|
|
string dataZoneId = ZONE_MP4_QT_CHAP_MDAT;
|
|
Zone chapMdatZone = structureHelper.GetZone(dataZoneId);
|
|
|
|
uint offset = (uint)(chapMdatZone.Offset + (isText ? 0 : totalTrackTxtSize));
|
|
structureHelper.AddPostProcessingIndex(w.BaseStream.Position, offset, false, dataZoneId, zoneId, zoneId);
|
|
w.Write(StreamUtils.EncodeBEUInt32(offset)); // TODO switch to co64 when needed ?
|
|
|
|
finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(stcoPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - stcoPos)));
|
|
w.BaseStream.Seek(finalFramePos, SeekOrigin.Begin);
|
|
// SAMPLE <-> CHUNK END
|
|
|
|
// SAMPLE TABLE END
|
|
// MEDIA INFORMATION END
|
|
// MEDIA END
|
|
|
|
finalFramePos = w.BaseStream.Position;
|
|
w.BaseStream.Seek(stblPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - stblPos)));
|
|
|
|
w.BaseStream.Seek(minfPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - minfPos)));
|
|
|
|
w.BaseStream.Seek(mdiaPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - mdiaPos)));
|
|
|
|
w.BaseStream.Seek(trakPos, SeekOrigin.Begin);
|
|
w.Write(StreamUtils.EncodeBEInt32((int)(finalFramePos - trakPos)));
|
|
|
|
return 1;
|
|
}
|
|
private int writeUuidFrame(TagData tag, string key, BinaryWriter w)
|
|
{
|
|
var keyNominal = key.Replace(" ", "");
|
|
if (keyNominal.Length < 32)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "uuid key should be 16-bit long");
|
|
return 0;
|
|
}
|
|
if (!Utils.IsHex(keyNominal))
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "uuid key should be given in hexadecimal format");
|
|
return 0;
|
|
}
|
|
|
|
byte[] data;
|
|
if (keyNominal.Equals(XmpTag.UUID_XMP, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
using var mem = new MemoryStream();
|
|
using var memW = new BinaryWriter(mem);
|
|
XmpTag.ToStream(memW, this);
|
|
data = mem.ToArray();
|
|
}
|
|
else
|
|
{
|
|
var info = tag.AdditionalFields.FirstOrDefault(f =>
|
|
"uuid." + keyNominal == f.NativeFieldCode && !f.MarkedForDeletion);
|
|
if (null == info)
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_ERROR,
|
|
"Couldn't find uuid " + keyNominal + " inside additionalFields");
|
|
return 0;
|
|
}
|
|
data = Encoding.UTF8.GetBytes(info.Value);
|
|
}
|
|
|
|
uint size = 8 + 16 + (uint)data.Length;
|
|
w.Write(StreamUtils.EncodeBEUInt32(size));
|
|
w.Write(Utils.Latin1Encoding.GetBytes("uuid"));
|
|
w.Write(Utils.ParseHex(keyNominal));
|
|
w.Write(data);
|
|
|
|
return 1;
|
|
}
|
|
|
|
private int writeUuidFrames(TagData tag, BinaryWriter w)
|
|
{
|
|
var existingUuids =
|
|
structureHelper.ZoneNames
|
|
.Where(n => n.StartsWith("uuid.", StringComparison.OrdinalIgnoreCase))
|
|
.Select(n => n[5..].Replace(" ", "").ToUpper());
|
|
|
|
var extraUuids = tag.AdditionalFields
|
|
.Where(f => f.TagType.Equals(MetaDataIOFactory.TagType.ANY) || f.TagType.Equals(getImplementedTagType()))
|
|
.Where(f => !f.MarkedForDeletion)
|
|
.Where(f => f.NativeFieldCode.StartsWith("uuid.", StringComparison.OrdinalIgnoreCase))
|
|
.Where(f => !existingUuids.Contains(f.NativeFieldCode[5..].Replace(" ", "").ToUpper()));
|
|
|
|
var written = 0;
|
|
foreach (var data in extraUuids)
|
|
{
|
|
written += writeUuidFrame(tag, data.NativeFieldCode[5..], w);
|
|
}
|
|
|
|
// Scan AdditionalData for the need to write an XMP UUID if there's none already set
|
|
if (!existingUuids.Contains(XmpTag.UUID_XMP))
|
|
{
|
|
foreach (MetaFieldInfo info in tag.AdditionalFields)
|
|
{
|
|
// XMP data => create the corresponding UUID atom to host it
|
|
if (info.NativeFieldCode.StartsWith("xmp.", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
written += writeUuidFrame(tag, XmpTag.UUID_XMP, w);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return written;
|
|
}
|
|
|
|
private static uint getMacDateNow()
|
|
{
|
|
DateTime date = DateTime.UtcNow;
|
|
DateTime date1904 = DateTime.Parse("1/1/1904 0:00:00 AM");
|
|
return (uint)date.Subtract(date1904).TotalSeconds;
|
|
}
|
|
|
|
private static byte[] encodeBEFixedPoint32(short intPart, short decPart)
|
|
{
|
|
return new[] {
|
|
(byte)((intPart & 0xFF00) >> 8), (byte)(intPart & 0x00FF),
|
|
(byte)((decPart & 0xFF00) >> 8), (byte)(decPart & 0x00FF)
|
|
};
|
|
}
|
|
|
|
// reduce the useful MDAT to a few Kbs (for dev purposes only)
|
|
|
|
#pragma warning disable S125 // Sections of code should not be commented out
|
|
/*
|
|
public override bool Remove(Stream s)
|
|
{
|
|
long chapDataSize = 0;
|
|
foreach (Zone zone in Zones)
|
|
{
|
|
if (zone.Name.Equals(ZONE_MP4_QT_CHAP_MDAT))
|
|
{
|
|
chapDataSize = zone.Size;
|
|
break;
|
|
}
|
|
}
|
|
s.BaseStream.Seek(AudioDataOffset, SeekOrigin.Begin);
|
|
long newSize = chapDataSize + 32000;
|
|
StreamUtils.WriteBEInt32(s, (int)newSize);
|
|
s.BaseStream.Seek(4 + newSize, SeekOrigin.Current);
|
|
StreamUtils.ShortenStream(s, AudioDataOffset + AudioDataSize, (uint)(AudioDataSize - newSize));
|
|
|
|
return true;
|
|
}
|
|
*/
|
|
}
|
|
#pragma warning restore S125 // Sections of code should not be commented out
|
|
} |