|
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using AVFoundation; |
|
using CoreMedia; |
|
using Foundation; |
|
using UIKit; |
|
|
|
namespace Chapters |
|
{ |
|
internal class ChapterReaderMp3 : IChapterReader |
|
{ |
|
private enum ID3Frame : uint |
|
{ |
|
ID3FrameEncoding = 1, |
|
ID3FrameShortDescription = 1, |
|
ID3FramePictureType = 1, |
|
ID3FrameFlags = 2, |
|
ID3FrameLanguage = 3, |
|
ID3FrameSize = 4, |
|
ID3FrameID = 4, |
|
ID3FrameFrame = 10 |
|
} |
|
|
|
private enum ID3FramePositions : uint |
|
{ |
|
ID3FramePositionID = 0, |
|
ID3FramePositionSize = ID3FramePositionID + ID3Frame.ID3FrameID, |
|
ID3FramePositionFlags = ID3FramePositionSize + ID3Frame.ID3FrameSize, |
|
ID3FramePositionEncoding = ID3FramePositionFlags + ID3Frame.ID3FrameFlags, |
|
ID3FramePositionText = ID3FramePositionEncoding + ID3Frame.ID3FrameEncoding |
|
} |
|
|
|
private enum ID3TextEncoding : uint |
|
{ |
|
ID3TextEncodingISO = 0, |
|
ID3TextEncodingUTF16 = 1, |
|
ID3TextEncodingUTF16BE = 2, |
|
ID3TextEncodingUTF8 = 3 |
|
} |
|
|
|
private const uint ID3HeaderSize = 4; |
|
private const string MetadataID3MetadataKeyChapter = "CHAP"; |
|
private const string MetadataID3MetadataKeyTableOfContents = "CTOC"; |
|
|
|
public IList<Chapter> GetChapters(AVAsset asset) |
|
{ |
|
var its = asset.GetMetadataForFormat(new NSString(Constants.MetadataFormatId3)); |
|
var items = AVMetadataItem.FilterWithKey( |
|
its, |
|
new NSString(MetadataID3MetadataKeyChapter), |
|
Constants.MetadataFormatId3); |
|
|
|
var chapterIdentifiers = GetTableOfContents(its); |
|
|
|
var chapters = new List<Chapter>(); |
|
|
|
foreach (var item in items) |
|
{ |
|
var chapter = CreateChapter(item.DataValue, chapterIdentifiers); |
|
chapters.Add(chapter); |
|
} |
|
return chapters.OrderBy(x => x.Time).ToList(); |
|
} |
|
|
|
private string[] GetTableOfContents(AVMetadataItem[] metadata) |
|
{ |
|
var tableOfContents = AVMetadataItem.FilterWithKey( |
|
metadata, |
|
new NSString(MetadataID3MetadataKeyTableOfContents), |
|
Constants.MetadataFormatId3); |
|
|
|
var toc = tableOfContents.First(); |
|
if (toc == null) return new string[]{}; |
|
|
|
var tocData = toc.DataValue; |
|
|
|
uint flagSize = 1; |
|
uint chapterCountSize = 1; |
|
uint index = (uint)GetDataToTerm(tocData).Length + flagSize; |
|
|
|
var numberOfChaptersData = tocData.Subdata(index, chapterCountSize); |
|
long numberOfChapters = 0; |
|
unsafe |
|
{ |
|
numberOfChapters = ByteToInt((byte*) numberOfChaptersData.Bytes, chapterCountSize, 0); |
|
} |
|
|
|
var chapterData = tocData.Subdata(index + chapterCountSize, (uint)(tocData.Length - chapterCountSize - index)); |
|
var chapterIdentifiers = new List<string>(); |
|
|
|
var splitData = SplitByTerminator(chapterData); |
|
|
|
if (numberOfChapters == 0) |
|
{ |
|
numberOfChapters = (uint) splitData.Count; |
|
} |
|
|
|
foreach (var subData in splitData.Take((int)numberOfChapters).ToList()) |
|
{ |
|
try |
|
{ |
|
var chapterIdentifier = NSString.FromData(subData, NSStringEncoding.UTF8); |
|
chapterIdentifiers.Add(chapterIdentifier); |
|
} |
|
catch (Exception e) |
|
{ |
|
// ignored |
|
} |
|
} |
|
|
|
return chapterIdentifiers.ToArray(); |
|
} |
|
|
|
private IList<NSData> SplitByTerminator(NSData data) |
|
{ |
|
uint maxLength = 1; |
|
byte[] buffer = new byte[maxLength]; |
|
bool terminated = false; |
|
NSInputStream stream = NSInputStream.FromData(data); |
|
|
|
IList<NSData> splitData = new List<NSData>(); |
|
|
|
stream.Open(); |
|
NSMutableData result = new NSMutableData(); |
|
while (stream.Read(buffer, maxLength) > 0) |
|
{ |
|
result.AppendBytes(buffer, 1); |
|
terminated = *(char *)buffer == '\0'; |
|
|
|
if (terminated) { |
|
[splitData addObject:result]; |
|
result = [NSMutableData new]; |
|
} |
|
} |
|
|
|
[stream close]; |
|
|
|
return [splitData copy]; |
|
} |
|
|
|
private Chapter CreateChapter(NSData data, string[] chapterIdentifiers) |
|
{ |
|
var identifierData = GetDataToTerm(data); |
|
var identifier = NSString.FromData(identifierData, NSStringEncoding.UTF8); |
|
|
|
var index = (uint)identifierData.Length; |
|
|
|
var startTimeData = data.Subdata(index, ID3HeaderSize); |
|
var endTimeData = data.Subdata(index += ID3HeaderSize, ID3HeaderSize); |
|
// var startOffsetData = data.Subdata(index += ID3HeaderSize, ID3HeaderSize); |
|
// var endOffsetData = data.Subdata(index += ID3HeaderSize, ID3HeaderSize); |
|
|
|
long startTime = 0; |
|
long endTime = 0; |
|
unsafe |
|
{ |
|
startTime = ByteToInt((byte*)startTimeData.Bytes.ToPointer(), (uint)startTimeData.Length, 0); |
|
endTime = ByteToInt((byte*)startTimeData.Bytes.ToPointer(), (uint)endTimeData.Length, 0); |
|
// startTimeOffset = ByteToInt((byte*)startTimeData.Bytes.ToPointer(), (uint)startTimeData.Length, 0); |
|
// endTimeOffset = ByteToInt((byte*)startTimeData.Bytes.ToPointer(), (uint)endTimeData.Length, 0); |
|
} |
|
|
|
var time = new CMTime(startTime, 1000); |
|
var duration = new CMTime(endTime - startTime, 1000); |
|
var chapter = new Chapter(time, duration) |
|
{ |
|
Identifier = identifier, |
|
IsHidden = chapterIdentifiers.Contains(identifier), |
|
Title = GetTitle(data), |
|
Url = GetUrl(data), |
|
Artwork = GetArtwork(data) |
|
}; |
|
return chapter; |
|
} |
|
|
|
private UIImage GetArtwork(NSData data) |
|
{ |
|
// UIImage *result = nil; |
|
// |
|
// @try { |
|
// NSRange range = [self rangeOfFrameWithID:AVMetadataID3MetadataKeyAttachedPicture inData:data]; |
|
// unsigned long loc = range.location; |
|
// |
|
// if (loc==NSNotFound) { |
|
// return nil; |
|
// } |
|
// |
|
// NSData *sizeData = SUBDATA(data, loc + ID3FramePositionSize, ID3FrameSize); |
|
// NSInteger size = btoi((char *)sizeData.bytes, sizeData.length, 0); |
|
// |
|
// // NSData *textEncodingData = SUBDATA(data, loc + ID3FramePositionEncoding, ID3FrameEncoding); |
|
// // NSInteger textEncodingValue = btoi((char *)textEncodingData.bytes, textEncodingData.length, 0); |
|
// // NSInteger textEncoding = [self textEncoding:textEncodingValue]; |
|
// |
|
// NSData *content = SUBDATA(data, loc + ID3FrameFrame + ID3FrameEncoding, size - ID3FrameEncoding); |
|
// |
|
// NSData *mimeTypeData = [self dataToTermInData:content]; |
|
// // NSString *mimeType = [NSString stringWithUTF8String:mimeTypeData.bytes]; |
|
// |
|
// content = SUBDATA(content, mimeTypeData.length+ID3FrameEncoding, content.length-mimeTypeData.length-ID3FrameEncoding); |
|
// |
|
// NSData *imageDescriptionData = [self dataToTermInData:content]; |
|
// // NSString *imageDescriptionText = [NSString stringWithUTF8String:imageDescriptionData.bytes]; |
|
// |
|
// content = SUBDATA(content, imageDescriptionData.length, content.length-imageDescriptionData.length); |
|
// |
|
// result = [UIImage imageWithData:content]; |
|
// } |
|
// @catch (NSException *exception) { |
|
// // |
|
// } |
|
// @finally { |
|
// return result; |
|
// } |
|
} |
|
|
|
private string GetUrl(NSData data) |
|
{ |
|
// NSString *result = nil; |
|
// |
|
// @try { |
|
// NSRange range = [self rangeOfFrameWithID:AVMetadataID3MetadataKeyUserURL inData:data]; |
|
// if (range.location == NSNotFound) { |
|
// return result; |
|
// } |
|
// |
|
// unsigned long loc = range.location; |
|
// |
|
// NSData *sizeData = SUBDATA(data, loc + ID3FramePositionSize, ID3FrameSize); |
|
// NSInteger size = btoi((char *)sizeData.bytes, sizeData.length, 0); |
|
// |
|
// NSData *encData = SUBDATA(data, loc + ID3FramePositionEncoding, ID3FrameEncoding); |
|
// NSInteger encValue = btoi((char *)encData.bytes, encData.length, 0); |
|
// NSInteger encoding = [self textEncoding:encValue]; |
|
// |
|
// NSData *content = SUBDATA(data, loc + ID3FrameFrame + ID3FrameEncoding, size - ID3FrameEncoding); |
|
// NSUInteger index = [self dataToTermInData:content].length; |
|
// NSData *url = SUBDATA(content, index, size - index - ID3FrameEncoding); |
|
// NSString *str = [[NSString alloc] initWithBytes:url.bytes length:url.length encoding:encoding]; |
|
// |
|
// result = [str stringByRemovingPercentEncoding]; |
|
// } @catch (NSException * e) { |
|
// // |
|
// } @finally { |
|
// return result; |
|
// } |
|
} |
|
|
|
private string GetTitle(NSData data) |
|
{ |
|
// NSString *result = nil; |
|
// @try { |
|
// NSRange range = [self rangeOfFrameWithID:AVMetadataID3MetadataKeyTitleDescription inData:data]; |
|
// unsigned long loc = range.location; |
|
// |
|
// NSData *sizeData = SUBDATA(data, loc + ID3FramePositionSize, ID3FrameSize); |
|
// NSUInteger size = btoi((char *)sizeData.bytes, sizeData.length, 0); |
|
// |
|
// NSData *encData = SUBDATA(data, loc + ID3FramePositionEncoding, ID3FrameEncoding); |
|
// NSInteger encValue = btoi((char *)encData.bytes, encData.length, 0); |
|
// NSInteger encoding = [self textEncoding:encValue]; |
|
// |
|
// NSData *titleData = SUBDATA(data, loc + ID3FramePositionText, size - ID3FrameEncoding); |
|
// |
|
// result = [[NSString alloc] initWithBytes:titleData.bytes |
|
// length:titleData.length |
|
// encoding:encoding]; |
|
// } |
|
// @catch (NSException *exception) { |
|
// // |
|
// } |
|
// @finally { |
|
// return result; |
|
// } |
|
} |
|
|
|
private NSData GetDataToTerm(NSData tocData) |
|
{ |
|
throw new System.NotImplementedException(); |
|
} |
|
|
|
private static unsafe bool IsSet(byte* bytes, uint size) |
|
{ |
|
var isSet = false; |
|
var index = size; |
|
while (index-- != 0 && !isSet) { |
|
isSet = bytes[index] != '\xff'; |
|
} |
|
return isSet; |
|
} |
|
|
|
private static unsafe long ByteToInt(byte* bytes, uint size, uint offset) |
|
{ |
|
int i; |
|
long result = 0x00; |
|
for(i = 0; i < size; i++) { |
|
result <<= 8; |
|
result |= bytes[offset + i]; |
|
} |
|
return result; |
|
} |
|
} |
|
} |
A few notes at first read :
MP4
See https://github.com/Zeugma440/atldotnet/wiki/Focus-on-Chapter-metadata for in-depth explanation
MP3
I've always found the
CTOC
frame to be accessory, even optional, even though the specs are unclear about it. You should consider theCHAP
frames to be your central source of information, withCTOC
providing additional descriptive data, not the other way around.Even though it might look like a good idea while reading the specs, relying on chapter identifiers might not work with badly formatted files and files with no
CTOC
frame. I'd advise you to organize your chapters according to their order, no more, no less. Imho, chapter identifiers from theCTOC
frame should be a descriptive attribute, not a cornerstone.Line 94 : Some people want to add more than 255 chapters and rely on the actual chapters count rather than the "entry count" field. I think your final number of chapters should rely on
splitData.Count
entirely.