-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathMp4.js
3195 lines (2693 loc) · 87.5 KB
/
Mp4.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const Default = 'Mp4.js';
export default Default;
import PromiseQueue from './PromiseQueue.js'
import {DataReader,DataWriter,EndOfFileMarker} from './DataReader.js'
import {StringToBytes,JoinTypedArrays} from './PopApi.js'
import * as H264 from './H264.js'
import {MP4,H264Remuxer} from './Mp4_Generator.js'
import {Debug,Warning,Yield} from './PopWebApiCore.js'
export class AtomDataReader extends DataReader
{
// gr: move this to an overloaded Atom/Mpeg DataReader
async ReadNextAtom(GetAtomType=null)
{
GetAtomType = GetAtomType || function(Fourcc) { return null; }
const Atom_FilePosition = this.ExternalFilePosition + this.FilePosition;
let Atom_Size;
// catch EOF and return null, instead of throwing
try
{
Atom_Size = await this.Read32();
}
catch(e)
{
if ( e == EndOfFileMarker )
return null;
throw e;
}
const Atom_Fourcc = await this.ReadString(4);
// alloc atom
const AtomType = GetAtomType(Atom_Fourcc) || Atom_t;
const Atom = new AtomType();
Atom.FilePosition = Atom_FilePosition;
Atom.Size = Atom_Size;
Atom.Fourcc = Atom_Fourcc;
// size of 1 means 64 bit size
if ( Atom.Size == 1 )
{
Atom.Size64 = await this.Read64();
}
if ( Atom.AtomSize < 8 )
throw `Atom (${Atom.Fourcc}) reported size as less than 8 bytes(${Atom.AtomSize}); not possible.`;
Atom.Data = await this.ReadBytes(Atom.ContentSize);
return Atom;
}
}
// gr: copied from c#
// so if either gets fixed, they both need to
// https://github.com/NewChromantics/PopCodecs/blob/7cecd65448aa7dececf7f4216b6b195b5b77f208/PopMpeg4.cs#L164
function GetDateTimeFromSecondsSinceMidnightJan1st1904(Seconds)
{
// todo: check this
//var Epoch = new DateTime(1904, 1, 1, 0, 0, 0, DateTimeKind.Utc);
//Epoch.AddSeconds(Seconds);
const Epoch = new Date('January 1, 1904 0:0:0 GMT');
// https://stackoverflow.com/questions/1197928/how-to-add-30-minutes-to-a-javascript-date-object
const EpochMilliSecs = Epoch.getTime();
const NewTime = new Date( EpochMilliSecs + (Seconds*1000) );
return NewTime;
}
// todo:
function GetSecondsSinceMidnightJan1st1904(TimeStamp)
{
const Epoch = new Date('January 1, 1904 0:0:0 GMT');
const DeltaMs = TimeStamp - Epoch;
const DeltaSecs = Math.floor(DeltaMs/1000);
return DeltaSecs;
}
// mp4 parser and ms docs contradict themselves
// these are bits for trun (fragment sample atoms)
// mp4 parser
const TrunFlags =
{
DataOffsetPresent: 0,
FirstSampleFlagsPresent: 2,
SampleDurationPresent: 8,
SampleSizePresent: 9,
SampleFlagsPresent: 10,
SampleCompositionTimeOffsetPresent: 11
};
/*
// ms (matching hololens stream)
enum TrunFlags
{
DataOffsetPresent = 0,
FirstSampleFlagsPresent = 3,
SampleDurationPresent = 9,
SampleSizePresent = 10,
SampleFlagsPresent = 11,
SampleCompositionTimeOffsetPresent = 12
};
*/
const SampleFlags =
{
// https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/formats/mp4/box_definitions.h#541
// sample_depends_on values in ISO/IEC 14496-12 Section 8.40.2.3.
// gr: used in chromium as (x>>24)&3 then cast to 0x3
// https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/formats/mp4/track_run_iterator.cc#189
//kSampleDependsOnUnknown = 0,
//kSampleDependsOnOthers = 1, bit 24 set
//kSampleDependsOnNoOther = 2, bit 25 set
//kSampleDependsOnReserved = 3, both set
DependsOnOthers: 24+0,
DependsOnNoOthers: 24+1, // "is depended on" in other cases, so is keyframe
isLeading: 24+2, //
IsNotKeyframe: 0+16,
PaddingValue: 1+16,
HasRedundancy: 4+16,
//IsDepedendedOn: 6+16,// keyframe
// last 2 bytes of flags are priority
DegredationPriority0: 0xff00,
DegredationPriority1: 0x00ff,
};
// todo? specific atom type encode&decoders?
class Sample_t
{
constructor()
{
this.DecodeTimeMs = null;
this.PresentationTimeMs = null;
this.IsKeyframe = true;
this.TrackId = null;
// decoder
this.DataSize;
this.DurationMs;
this.DataPosition;
this.DataFilePosition;
// encoder
this.Data;
this.CompositionTimeOffset;
}
get Flags()
{
let Flags = 0;
if ( this.IsKeyframe )
{
Flags |= 1<<SampleFlags.DependsOnNoOthers;
}
else
{
Flags |= 1<<SampleFlags.IsNotKeyframe;
Flags |= 1<<SampleFlags.DependsOnOthers;
}
return Flags;
}
set Flags(Flags)
{
const NotKeyframe = Flags & (1<<SampleFlags.IsNotKeyframe);
const DependsOnOthers = Flags & (1<<SampleFlags.DependsOnOthers);
// in case of bad flags, assume keyframe?
this.IsKeyframe = (!NotKeyframe) || (!DependsOnOthers);
}
get Size()
{
if ( this.Data )
return this.Data.length;
return this.DataSize;
}
}
export class Atom_t
{
constructor(Fourcc=null,CopyAtom=null)
{
this.Size = 0; // total size
this.FilePosition = null;
this.Fourcc = Fourcc; // string of four chars
this.Size64 = null; // only set if Size=1
this.Data = null; // raw data following this header
this.ChildAtoms = []; // more Atom_t's (key these? can there be duplicates?)
if ( CopyAtom )
{
if ( CopyAtom.Fourcc != this.Fourcc )
throw `Copying atom with different fourcc (${this.Fourcc} -> ${CopyAtom.Fourcc})`;
// gr: do deep copy of child atoms?
Object.assign(this,CopyAtom);
}
}
get DataFilePosition()
{
return this.FilePosition + this.HeaderSize;
}
get HeaderSize()
{
let Size = 0;
Size += (32/8); // .Size
Size += 4; // .Fourcc
// 64bit size
if ( this.Size == 1 )
Size += (64/8);
return Size;
}
get AtomSize()
{
return (this.Size==1) ? this.Size64 : this.Size;
}
get ContentSize()
{
return this.AtomSize - this.HeaderSize;
}
// if this is an atom with child atoms, parse the next level here
async DecodeChildAtoms()
{
const Reader = new AtomDataReader(this.Data,this.DataFilePosition);
while ( Reader.FilePosition < this.Data.length )
{
const Atom = await Reader.ReadNextAtom();
this.ChildAtoms.push(Atom);
}
}
GetChildAtom(Fourcc)
{
const Matches = this.ChildAtoms.filter( a => a.Fourcc == Fourcc );
if ( Matches.length == 0 )
return null;
if ( Matches.length > 1 )
throw `More than one(x${Matches.length}) child ${Fourcc}} atom found`;
return Matches[0];
}
GetChildAtoms(Fourcc)
{
const Matches = this.ChildAtoms.filter( a => a.Fourcc == Fourcc );
return Matches;
}
// turn atom[tree] into Uint8Array()
Encode(IncludeAtomHeader=true)
{
if ( this.Fourcc.length != 4 )
throw `Atom fourcc (${this.Fourcc}) is not 4 chars`;
// bake sub data
const SubDataWriter = new DataWriter();
this.EncodeData(SubDataWriter);
const Data = SubDataWriter.GetData();
if ( !IncludeAtomHeader )
return Data;
// atom size includes header size
let AtomSize = (32/8) + 4; // size + fourcc
AtomSize += Data.length;
if ( AtomSize > 0xffffffff )
{
AtomSize += 64/8;
this.Size64 = AtomSize;
this.Size = 1;
}
else
{
this.Size64 = null;
this.Size = AtomSize;
}
// write out atom header+data
const Writer = new DataWriter();
Writer.Write32(this.Size);
Writer.WriteStringAsBytes(this.Fourcc);
if ( this.Size64 !== null )
Writer.Write64(this.Size64);
Writer.WriteBytes(Data);
const AtomData = Writer.GetData();
return AtomData;
}
// default, overload if not writing child atoms or dumb data
EncodeData(DataWriter)
{
if ( this.ChildAtoms.length )
{
if ( this.Data )
throw `Atom has child nodes AND data, should only have one`;
for ( let ChildAtom of this.ChildAtoms )
{
const ChildAtomAsData = ChildAtom.Encode();
DataWriter.WriteBytes(ChildAtomAsData);
}
return;
}
if ( !this.Data )
{
//throw `Atom has no data`;
}
else
{
DataWriter.WriteBytes( this.Data );
}
}
};
function GetSampleHash(Sample)
{
// we can use the file position for a unique hash for a sample
// gr: make sure this doesnt fall over with fragmented mp4s
const FilePosition = Sample.DataFilePosition;
if ( FilePosition === undefined )
return false;
return FilePosition;
}
/*
this is an async (stream data in, async chunks out)
mp4 decoder, based on my C#/unity one https://github.com/NewChromantics/PopCodecs/blob/master/PopMpeg4.cs
probably not perfect, but hand made to work around random weird/badly constructed mpeg files
*/
export class Mp4Decoder
{
constructor()
{
// gonna end up with a bunch of different version of these for debugging
this.NewAtomQueue = new PromiseQueue('Mp4 decoded atoms');
this.NewTrackQueue = new PromiseQueue('Mpeg decoded Tracks');
this.NewSamplesQueue = new PromiseQueue('Mpeg Decoded samples');
this.SamplesAlreadyOutput = {}; // [SampleHash] if defined, this sample has already been output
this.PendingAtoms = []; // pre-decoded atoms pushed into file externally
this.RootAtoms = []; // trees coming off root atoms
this.Mdats = []; // atoms with data
this.Tracks = [];
this.NewByteQueue = new PromiseQueue('Mp4 pending bytes');
this.FileReader = new AtomDataReader( new Uint8Array(0), 0, this.WaitForMoreFileData.bind(this) );
this.ParsePromise = this.ParseFileThread();
this.ParsePromise.catch( this.OnError.bind(this) );
}
OnError(Error)
{
// make queues fail
Warning(`Mp4 decode thread error ${Error}`);
this.NewSamplesQueue.Reject(Error);
this.NewAtomQueue.Reject(Error);
this.NewTrackQueue.Reject(Error);
}
// gr: should this integrate into WaitForNextSamples?
async WaitForParseFinish()
{
return this.ParsePromise;
}
// any atom at all
// may want
// - WaitForNewRootAtom (completed)
// - WaitForNewMdat (ie, new chunks of real parsed data)
async WaitForNextAtom()
{
return this.NewAtomQueue.WaitForNext();
}
async WaitForChange()
{
await this.NewAtomQueue.WaitForNext();
return this.RootAtoms;
}
async WaitForNextSamples()
{
return this.NewSamplesQueue.WaitForNext();
}
async WaitForMoreFileData()
{
return this.NewByteQueue.WaitForNext();
}
OnNewSamples(Samples)
{
// null == EOF
if ( !Samples )
{
this.NewSamplesQueue.Push( Samples );
return;
}
function VerifySample(Sample)
{
if ( !Number.isInteger(Sample.TrackId) )
throw `Sample has invalid track id ${Sample.TrackId}`;
}
// detect bad sample input
Samples.forEach(VerifySample);
// remove samples we've already output
// we need this because if we inject a tail-moov and output samples,
// when the file comes across that moov again, it processes them and outputs new samples
// todo: somehow detect that atom is a duplicate and skip the decoding of the sample table
// and just use this as a failsafe
function HasOutputSample(Sample)
{
const Hash = GetSampleHash(Sample);
// unhashable samples (eg dynamic SPS/PPS) don't get filtered
if ( !Hash )
return false;
if ( this.SamplesAlreadyOutput.hasOwnProperty(Hash) )
return true;
return false;
}
Samples = Samples.filter( Sample => !HasOutputSample.call(this,Sample) );
// all samples filtered out
if ( !Samples.length )
return;
function MarkSampleOutput(Hash)
{
this.SamplesAlreadyOutput[Hash] = true;
}
// mark samples as output
const SampleHashs = Samples.map( GetSampleHash ).filter( Hash => Hash!=null );
SampleHashs.forEach(MarkSampleOutput.bind(this));
this.NewSamplesQueue.Push( Samples );
}
PushEndOfFile()
{
this.PushData(EndOfFileMarker);
}
PushData(Bytes)
{
// we now allow caller to push in pre-decoded atoms
// eg. MOOV extracted from tail of an mp4
// gr: when data moves through web workers, it loses
// it's type. so check for our Atom_t member[s]
if ( Bytes.hasOwnProperty('Fourcc') )
{
const Atom = new Atom_t();
Object.assign( Atom, Bytes );
Bytes = Atom;
}
if ( Bytes instanceof Atom_t )
{
this.PendingAtoms.push(Bytes);
return;
}
// check valid input types
if ( Bytes == EndOfFileMarker )
{
}
else if ( Bytes instanceof Uint8Array )
{
}
else
{
throw `PushData(${typeof Bytes}) to mp4 which isn't a byte array`;
}
if ( !Bytes )
throw `Don't pass null to Mp4 PushData(), call PushEndOfFile()`;
this.NewByteQueue.Push(Bytes);
}
PushMdat(MdatAtom)
{
this.Mdats.push(MdatAtom);
}
async ReadNextAtom()
{
if ( this.PendingAtoms.length > 0 )
{
const Atom = this.PendingAtoms.shift();
return Atom;
}
const Atom = await this.FileReader.ReadNextAtom();
return Atom;
}
async ParseFileThread()
{
while ( true )
{
const Atom = await this.ReadNextAtom();
if ( Atom === null )
{
//Debug(`End of file`);
break;
}
this.RootAtoms.push(Atom);
this.NewAtomQueue.Push(Atom);
if ( Atom.Fourcc == 'ftyp' )
{
await this.DecodeAtom_Ftyp(Atom);
}
else if ( Atom.Fourcc == 'moov' )
{
await this.DecodeAtom_Moov(Atom);
}
else if ( Atom.Fourcc == 'moof' )
{
await this.DecodeAtom_Moof(Atom);
}
else if ( Atom.Fourcc == 'mdat' )
{
await this.DecodeAtom_Mdat(Atom);
}
else
{
//Debug(`Skipping atom ${Atom.Fourcc} x${Atom.ContentSize}`);
}
// breath
await Yield(0);
}
// push a null eof sample when parsing done
this.OnNewSamples(null);
}
OnNewTrack(Track)
{
this.Tracks.push(Track);
this.NewTrackQueue.Push(Track);
}
GetTrack(TrackId)
{
const Track = this.Tracks.find( t => t.Id == TrackId );
if ( !Track )
throw `No track ${TrackId}`;
return Track;
}
async DecodeAtom_Mdat(Atom)
{
this.PushMdat(Atom);
}
async DecodeAtom_MoofHeader(Atom)
{
const Header = {};
if ( !Atom )
return Header;
const Reader = new AtomDataReader(Atom.Data,Atom.DataFilePosition);
const Version = await Reader.Read8();
const Flags = await Reader.Read24();
Header.SequenceNumber = await Reader.Read32();
return Header;
}
async DecodeAtom_Moof(Atom)
{
await Atom.DecodeChildAtoms();
Atom.ChildAtoms.forEach( a => this.NewAtomQueue.Push(a) );
let Header = await this.DecodeAtom_MoofHeader( Atom.GetChildAtom('mfhd') );
if ( !Header )
{
Header = {};
}
// gr: units are milliseconds in moof
// 30fps = 33.33ms = [512, 1024, 1536, 2048...]
// 193000ms = 2959360
Header.TimeScale = 1.0 / 15333.4;
const TrackFragmentAtoms = Atom.GetChildAtoms('traf');
for ( const TrackFragmentAtom of TrackFragmentAtoms )
{
const MdatIdent = null;
const Track = await this.DecodeAtom_TrackFragment( TrackFragmentAtom, Atom, Header, MdatIdent );
this.OnNewTrack(Track);
}
}
async DecodeAtom_TrackFragment(Atom,MoofAtom,MoofHeader,MdatIdent)
{
await Atom.DecodeChildAtoms();
Atom.ChildAtoms.forEach( a => this.NewAtomQueue.Push(a) );
const Tfhd = Atom.GetChildAtom('tfhd'); // header
const Tfdt = Atom.GetChildAtom('tfdt'); // delta time
const Header = await this.DecodeAtom_TrackFragmentHeader(Tfhd,Tfdt);
Header.TimeScale = MoofHeader.TimeScale;
const Trun = Atom.GetChildAtom('trun');
const Samples = await this.DecodeAtom_FragmentSampleTable( Trun, MoofAtom, Header );
this.OnNewSamples(Samples);
}
async DecodeAtom_TrackFragmentDelta(Atom)
{
if ( !Atom )
return 0;
const Tfdt = await Atom_Tfdt.Read(Atom);
return Tfdt.DecodeTime;
}
async DecodeAtom_TrackFragmentHeader(Atom,DeltaTimeAtom)
{
if ( !Atom )
return new Atom_Tfhd('tfhd');
const tfhd = await Atom_Tfhd.Read(Atom);
//Header.DecodeTime = await this.DecodeAtom_TrackFragmentDelta(DeltaTimeAtom);
return tfhd;
}
async DecodeAtom_FragmentSampleTable(Atom,MoofAtom,TrackHeader)
{
if ( !Atom )
return [];
const Header = TrackHeader;
const Reader = new AtomDataReader(Atom.Data,Atom.DataFilePosition);
// this stsd description isn't well documented on the apple docs
// http://xhelmboyx.tripod.com/formats/mp4-layout.txt
// https://stackoverflow.com/a/14549784/355753
const Version = await Reader.Read8();
const Flags = await Reader.Read24();
const EntryCount = await Reader.Read32();
// gr; with a fragmented mp4 the headers were incorrect (bad sample sizes, mismatch from mp4parser's output)
// ffmpeg -i cat_baseline.mp4 -c copy -movflags frag_keyframe+empty_moov cat_baseline_fragment.mp4
// http://178.62.222.88/mp4parser/mp4.js
// so trying this version
// VERSION8
// FLAGS24
// SAMPLECOUNT32
// https://msdn.microsoft.com/en-us/library/ff469478.aspx
// the docs on which flags are which are very confusing (they list either 25 bits or 114 or I don't know what)
// 0x0800 is composition|size|duration
// from a stackoverflow post, 0x0300 is size|duration
// 0x0001 is offset from http://mp4parser.com/
function IsFlagBit(Bit) { return (Flags & (1 << Bit)) != 0; };
const SampleSizePresent = IsFlagBit(TrunFlags.SampleSizePresent);
const SampleDurationPresent = IsFlagBit(TrunFlags.SampleDurationPresent);
const SampleFlagsPresent = IsFlagBit(TrunFlags.SampleFlagsPresent);
const SampleCompositionTimeOffsetPresent = IsFlagBit(TrunFlags.SampleCompositionTimeOffsetPresent);
const FirstSampleFlagsPresent = IsFlagBit(TrunFlags.FirstSampleFlagsPresent);
const DataOffsetPresent = IsFlagBit(TrunFlags.DataOffsetPresent);
// This field MUST be set.It specifies the offset from the beginning of the MoofBox field(section 2.2.4.1).
// gr:... to what?
// If only one TrunBox is specified, then the DataOffset field MUST be the sum of the lengths of the MoofBox and all the fields in the MdatBox field(section 2.2.4.8).
// basically, start of mdat data (which we know anyway)
if (!DataOffsetPresent)
throw "Expected data offset to be always set";
const DataOffsetFromMoof = await (DataOffsetPresent ? Reader.Read32() : 0 );
function TimeToMs(TimeUnit)
{
// to float
const Timef = TimeUnit * Header.TimeScale;
const TimeMs = Timef * 1000.0;
return Math.floor(TimeMs);
};
// DataOffset(4 bytes): This field MUST be set.It specifies the offset from the beginning of the MoofBox field(section 2.2.4.1).
// If only one TrunBox is specified, then the DataOffset field MUST be the sum of the lengths of the MoofBo
// gr: we want the offset into the mdat, but we would have to ASSUME the mdat follows this moof
// just for safety, we work out the file offset instead, as we know where the start of the moof is
if (Header.BaseDataOffset !== undefined )
{
const HeaderPos = Header.BaseDataOffset;
const MoofPos = MoofAtom.FilePosition;
if (HeaderPos != MoofPos)
{
Debug("Expected Header Pos(" + HeaderPos + ") and moof pos(" + MoofPos + ") to be the same");
Header.BaseDataOffset = MoofPos;
}
}
const MoofPosition = (Header.BaseDataOffset!==undefined) ? Header.BaseDataOffset : MoofAtom.FilePosition;
const DataFileOffset = MoofPosition + DataOffsetFromMoof;
const Samples = []; // sample_t
let CurrentDataStartPosition = DataFileOffset;
let CurrentTime = (Header.DecodeTime!==undefined) ? Header.DecodeTime : 0;
let FirstSampleFlags = 0;
if (FirstSampleFlagsPresent )
{
FirstSampleFlags = await Reader.Read32();
}
// when the fragments are really split up into 1sample:1dat a different box specifies values
let DefaultSampleDuration = Header.DefaultSampleDuration || 0;
let DefaultSampleSize = Header.DefaultSampleSize || 0;
let DefaultSampleFlags = Header.DefaultSampleFlags || 0;
const Track = this.GetTrack(TrackHeader.TrackId);
for ( let sd=0; sd<EntryCount; sd++)
{
let SampleDuration = await (SampleDurationPresent ? Reader.Read32() : DefaultSampleDuration);
let SampleSize = await (SampleSizePresent ? Reader.Read32() : DefaultSampleSize);
let TrunBoxSampleFlags = await (SampleFlagsPresent ? Reader.Read32() : DefaultSampleFlags);
let SampleCompositionTimeOffset = await (SampleCompositionTimeOffsetPresent ? Reader.Read32() : 0 );
if (SampleCompositionTimeOffsetPresent)
{
// correct CurrentTimeMs?
}
const Sample = new Sample_t();
//Sample.MDatIdent = MDatIdent.HasValue ? MDatIdent.Value : -1;
Sample.DataFilePosition = CurrentDataStartPosition;
Sample.DataSize = SampleSize;
Sample.DurationMs = TimeToMs(SampleDuration);
Sample.DecodeTimeMs = TimeToMs(CurrentTime);
Sample.PresentationTimeMs = TimeToMs(CurrentTime+SampleCompositionTimeOffset);
Sample.Flags = TrunBoxSampleFlags;
Sample.TrackId = TrackHeader.TrackId;
Sample.ContentType = Track.ContentType;
Samples.push(Sample);
CurrentTime += SampleDuration;
CurrentDataStartPosition += SampleSize;
}
return Samples;
}
async DecodeAtom_Ftyp(Atom)
{
const Reader = new AtomDataReader(Atom.Data,Atom.DataFilePosition);
const MajorBrand = await Reader.ReadString(4);
const MinorVersion = await Reader.Read32();
//Debug(`ftyp ${MajorBrand} ver 0x${MinorVersion.toString(16)}`);
}
async DecodeAtom_Moov(Atom)
{
await Atom.DecodeChildAtoms();
Atom.ChildAtoms.forEach( a => this.NewAtomQueue.Push(a) );
const MovieHeaderAtom = Atom.GetChildAtom("mvhd");
let MovieHeader;
if ( MovieHeaderAtom )
{
MovieHeader = await this.DecodeAtom_MovieHeader(MovieHeaderAtom);
}
// now go through all the trak children
const TrakAtoms = Atom.GetChildAtoms('trak');
for ( let TrakAtom of TrakAtoms )
{
const Track = await this.DecodeAtom_Trak(TrakAtom,MovieHeader);
this.OnNewTrack(Track);
}
}
// gr; this doesn tneed to be async as we have the data, but all the reader funcs currently are
async DecodeAtom_MovieHeader(Atom)
{
const Reader = new AtomDataReader(Atom.Data,Atom.DataFilePosition);
// https://developer.apple.com/library/content/documentation/QuickTime/QTFF/art/qt_l_095.gif
const Version = await Reader.Read8();
const Flags = await Reader.Read24();
// hololens had what looked like 64 bit timestamps...
// this is my working reference :)
// https://github.com/macmade/MP4Parse/blob/master/source/MP4.MVHD.cpp#L50
let CreationTime,ModificationTime,Duration; // long
let TimeScale;
if ( Version == 0)
{
CreationTime = await Reader.Read32();
ModificationTime = await Reader.Read32();
TimeScale = await Reader.Read32();
Duration = await Reader.Read32();
}
else if(Version == 1)
{
CreationTime = await Reader.Read64();
ModificationTime = await Reader.Read64();
TimeScale = await Reader.Read32();
Duration = await Reader.Read64();
}
else
{
throw `Expected Version 0 or 1 for MVHD (Version=${Version}). If neccessary can probably continue without timing info!`;
}
const PreferredRate = await Reader.Read32();
const PreferredVolume = await Reader.Read16(); // 8.8 fixed point volume
const Reserved = await Reader.ReadBytes(10);
const Matrix_a = await Reader.Read32();
const Matrix_b = await Reader.Read32();
const Matrix_u = await Reader.Read32();
const Matrix_c = await Reader.Read32();
const Matrix_d = await Reader.Read32();
const Matrix_v = await Reader.Read32();
const Matrix_x = await Reader.Read32();
const Matrix_y = await Reader.Read32();
const Matrix_w = await Reader.Read32();
const PreviewTime = await Reader.Read32();
const PreviewDuration = await Reader.Read32();
const PosterTime = await Reader.Read32();
const SelectionTime = await Reader.Read32();
const SelectionDuration = await Reader.Read32();
const CurrentTime = await Reader.Read32();
const NextTrackId = await Reader.Read32();
for ( const Zero of Reserved )
{
if (Zero != 0)
Warning(`Reserved value ${Zero} is not zero`);
}
// actually a 3x3 matrix, but we make it 4x4 for unity
// gr: do we need to transpose this? docs don't say row or column major :/
// wierd element labels, right? spec uses them.
/*
// gr: matrixes arent simple
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737
// All values in the matrix are 32 - bit fixed-point numbers divided as 16.16, except for the { u, v, w}
// column, which contains 32 - bit fixed-point numbers divided as 2.30.Figure 5 - 1 and Figure 5 - 2 depict how QuickTime uses matrices to transform displayed objects.
var a = Fixed1616ToFloat(Matrix_a);
var b = Fixed1616ToFloat(Matrix_b);
var u = Fixed230ToFloat(Matrix_u);
var c = Fixed1616ToFloat(Matrix_c);
var d = Fixed1616ToFloat(Matrix_d);
var v = Fixed230ToFloat(Matrix_v);
var x = Fixed1616ToFloat(Matrix_x);
var y = Fixed1616ToFloat(Matrix_y);
var w = Fixed230ToFloat(Matrix_w);
var MtxRow0 = new Vector4(a, b, u, 0);
var MtxRow1 = new Vector4(c, d, v, 0);
var MtxRow2 = new Vector4(x, y, w, 0);
var MtxRow3 = new Vector4(0, 0, 0, 1);
*/
const Header = {};
//var Header = new TMovieHeader();
Header.TimeScale = 1.0 / TimeScale; // timescale is time units per second
//Header.VideoTransform = new Matrix4x4(MtxRow0, MtxRow1, MtxRow2, MtxRow3);
//Header.Duration = new TimeSpan(0,0,(int)(Duration * Header.TimeScale));
Header.Duration = Duration * Header.TimeScale;
Header.CreationTime = GetDateTimeFromSecondsSinceMidnightJan1st1904(CreationTime);
Header.ModificationTime = GetDateTimeFromSecondsSinceMidnightJan1st1904(ModificationTime);
Header.PreviewDuration = PreviewDuration * Header.TimeScale;
return Header;
}
async DecodeAtom_Trak(Atom,MovieHeader)
{
await Atom.DecodeChildAtoms();
Atom.ChildAtoms.forEach( a => this.NewAtomQueue.Push(a) );
const TrackHeader = await Atom_Tkhd.Read( Atom.GetChildAtom('tkhd') );
const Track = {};
Track.Id = TrackHeader.TrackId;
const Medias = [];
const MediaAtoms = Atom.GetChildAtoms('mdia');
for ( let MediaAtom of MediaAtoms )
{
const Media = await this.DecodeAtom_Media( MediaAtom, Track, MovieHeader );
Medias.push(Media);
}
Track.Medias = Medias;
Track.ContentType = Object.keys(Medias[0].MediaInfo)[0];
//Debug(`Found x${Medias.length} media atoms`);
return Track;
}
async DecodeAtom_Media(Atom,Track,MovieHeader)
{
await Atom.DecodeChildAtoms();
Atom.ChildAtoms.forEach( a => this.NewAtomQueue.Push(a) );
const Media = {};
// these may not exist
Media.MediaHeader = await this.DecodeAtom_MediaHandlerHeader( Atom.GetChildAtom('mdhd') );
// defaults (this timescale should come from further up)
if ( !Media.MediaHeader )
{
Media.MediaHeader = {};
Media.MediaHeader.TimeScale = 1000;
}
Media.MediaInfo = await this.DecodeAtom_MediaInfo( Atom.GetChildAtom('minf'), Track.Id, Media.MediaHeader, MovieHeader );
return Media;
}
async DecodeAtom_MediaHandlerHeader(Atom)
{
if ( !Atom )
return null;
const Reader = new AtomDataReader(Atom.Data,Atom.DataFilePosition);
const Version = await Reader.Read8();
const Flags = await Reader.Read24();
const CreationTime = await Reader.Read32();
const ModificationTime = await Reader.Read32();
const TimeScale = await Reader.Read32();
const Duration = await Reader.Read32();
const Language = await Reader.Read16();
const Quality = await Reader.Read16();
const Header = {};//new TMediaHeader();
Header.TimeScale = 1.0 / TimeScale; // timescale is time units per second
//Header.Duration = new TimeSpan(0,0, (int)(Duration * Header.TimeScale));
Header.CreationTime = GetDateTimeFromSecondsSinceMidnightJan1st1904(CreationTime);
Header.ModificationTime = GetDateTimeFromSecondsSinceMidnightJan1st1904(ModificationTime);
Header.LanguageId = Language;
Header.Quality = Quality / (1 << 16);
return Header;
}
async DecodeAtom_MediaInfo(Atom,TrackId,MediaHeader,MovieHeader)
{
if ( !Atom )
return null;
await Atom.DecodeChildAtoms();
Atom.ChildAtoms.forEach( a => this.NewAtomQueue.Push(a) );
const Samples = await this.DecodeAtom_SampleTable( Atom.GetChildAtom('stbl'), TrackId, MediaHeader, MovieHeader );
const Dinfs = Atom.GetChildAtoms('dinf');
for ( let Dinf of Dinfs )
await this.DecodeAtom_Dinf(Dinf);
const MediaInfo = {};
// subtitle meta
const Tx3g = MediaHeader.SampleMeta.GetChildAtom('tx3g');
if ( Tx3g )
{
MediaInfo.Subtitle = Tx3g;
}
// todo: should we convert header to samples? (SPS & PPS)
// does this ONLY apply to h264/video?
const Avc1 = MediaHeader.SampleMeta.GetChildAtom('avc1');
if ( Avc1 )
{
const Avcc = Avc1.GetChildAtom('avcC');
if ( Avcc )
{
const ContentType = 'H264';
MediaInfo[ContentType] = Avcc;