@bubo-squared/gyroview 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +35 -15
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2 -2
- package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
- package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
- package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
- package/dist/packages/adapters/fetch/src/index.js +3 -1
- package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
- package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
- package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
- package/dist/packages/adapters/mediabunny/src/index.js +3 -2
- package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +12 -3
- package/dist/packages/core/src/application/download/BlockStore.js +97 -0
- package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
- package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
- package/dist/packages/core/src/application/download/FileDownload.js +183 -0
- package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
- package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
- package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
- package/dist/packages/core/src/application/download/Transfers.js +75 -0
- package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
- package/dist/packages/core/src/application/playback/Buffering.js +53 -0
- package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
- package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
- package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
- package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
- package/dist/packages/core/src/application/playback/probeDecoding.js +17 -3
- package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
- package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
- package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
- package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
- package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
- package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
- package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
- package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
- package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
- package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
- package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
- package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
- package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
- package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
- package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
- package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
- package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
- package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
- package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
- package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
- package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
- package/dist/packages/core/src/index.js +10 -2
- package/dist/packages/core/src/shared/async/iteration.js +24 -0
- package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
- package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
- package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
- package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
- package/dist/packages/core/src/shared/errors/GyroViewError.js +56 -3
- package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
- package/dist/packages/player/src/composition/browserPorts.js +23 -4
- package/dist/packages/player/src/composition/buildPipeline.js +15 -3
- package/dist/packages/player/src/composition/openInputs.js +51 -107
- package/dist/packages/player/src/composition/readInputs.js +111 -0
- package/dist/packages/player/src/controls/messages.js +3 -1
- package/dist/packages/player/src/inspectRecording.js +1 -1
- package/dist/standalone.js +58 -58
- package/package.json +1 -1
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
- package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/format/mp4/mp4Layouts.ts
|
|
2
|
+
/**
|
|
3
|
+
* MovieHeaderBox, `mvhd` (§8.2.2): creation and modification times, the movie timescale (in which
|
|
4
|
+
* edit lists count), the duration, then fields the player does not read.
|
|
5
|
+
*/
|
|
6
|
+
var MOVIE_HEADER = {
|
|
7
|
+
0: {
|
|
8
|
+
timescale: 8,
|
|
9
|
+
duration: {
|
|
10
|
+
offset: 12,
|
|
11
|
+
kind: "uint32"
|
|
12
|
+
},
|
|
13
|
+
size: 96
|
|
14
|
+
},
|
|
15
|
+
1: {
|
|
16
|
+
timescale: 16,
|
|
17
|
+
duration: {
|
|
18
|
+
offset: 20,
|
|
19
|
+
kind: "uint64"
|
|
20
|
+
},
|
|
21
|
+
size: 108
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* TrackHeaderBox, `tkhd` (§8.3.2): the track's id sits after the creation and modification times.
|
|
26
|
+
*/
|
|
27
|
+
var TRACK_HEADER_ID = {
|
|
28
|
+
0: 8,
|
|
29
|
+
1: 16
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* MediaHeaderBox, `mdhd` (§8.4.2): the timescale the track's sample tables count in.
|
|
33
|
+
*/
|
|
34
|
+
var MEDIA_HEADER = {
|
|
35
|
+
0: {
|
|
36
|
+
timescale: 8,
|
|
37
|
+
duration: {
|
|
38
|
+
offset: 12,
|
|
39
|
+
kind: "uint32"
|
|
40
|
+
},
|
|
41
|
+
size: 20
|
|
42
|
+
},
|
|
43
|
+
1: {
|
|
44
|
+
timescale: 16,
|
|
45
|
+
duration: {
|
|
46
|
+
offset: 20,
|
|
47
|
+
kind: "uint64"
|
|
48
|
+
},
|
|
49
|
+
size: 32
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* TimeToSampleBox, `stts` (§8.6.1.2): runs of samples with the same decode duration.
|
|
54
|
+
*/
|
|
55
|
+
var TIME_TO_SAMPLE = {
|
|
56
|
+
entrySize: 8,
|
|
57
|
+
fields: {
|
|
58
|
+
sampleCount: {
|
|
59
|
+
offset: 0,
|
|
60
|
+
kind: "uint32"
|
|
61
|
+
},
|
|
62
|
+
sampleDelta: {
|
|
63
|
+
offset: 4,
|
|
64
|
+
kind: "uint32"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* CompositionOffsetBox, `ctts` (§8.6.1.3): runs of samples presented the same time after (and in
|
|
70
|
+
* version 1, possibly before) they are decoded. Version 0 is unsigned by the standard, but
|
|
71
|
+
* writers put negative offsets there too, so both versions are read signed, as mediabunny and
|
|
72
|
+
* FFmpeg read them.
|
|
73
|
+
*/
|
|
74
|
+
var COMPOSITION_OFFSET = {
|
|
75
|
+
entrySize: 8,
|
|
76
|
+
fields: {
|
|
77
|
+
sampleCount: {
|
|
78
|
+
offset: 0,
|
|
79
|
+
kind: "uint32"
|
|
80
|
+
},
|
|
81
|
+
sampleOffset: {
|
|
82
|
+
offset: 4,
|
|
83
|
+
kind: "int32"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* SyncSampleBox, `stss` (§8.6.2): the 1-based numbers of the samples decoding may start at. A
|
|
89
|
+
* track without one starts anywhere.
|
|
90
|
+
*/
|
|
91
|
+
var SYNC_SAMPLE = {
|
|
92
|
+
entrySize: 4,
|
|
93
|
+
fields: { sampleNumber: {
|
|
94
|
+
offset: 0,
|
|
95
|
+
kind: "uint32"
|
|
96
|
+
} }
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* EditListBox, `elst` (§8.6.6): segments of the movie timeline, each showing the media from
|
|
100
|
+
* `mediaTime` (media timescale) for `segmentDuration` (movie timescale), or nothing where
|
|
101
|
+
* `mediaTime` is -1.
|
|
102
|
+
*/
|
|
103
|
+
var EDIT_LIST = {
|
|
104
|
+
0: {
|
|
105
|
+
entrySize: 12,
|
|
106
|
+
fields: {
|
|
107
|
+
segmentDuration: {
|
|
108
|
+
offset: 0,
|
|
109
|
+
kind: "uint32"
|
|
110
|
+
},
|
|
111
|
+
mediaTime: {
|
|
112
|
+
offset: 4,
|
|
113
|
+
kind: "int32"
|
|
114
|
+
},
|
|
115
|
+
mediaRate: {
|
|
116
|
+
offset: 8,
|
|
117
|
+
kind: "uint32"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
1: {
|
|
122
|
+
entrySize: 20,
|
|
123
|
+
fields: {
|
|
124
|
+
segmentDuration: {
|
|
125
|
+
offset: 0,
|
|
126
|
+
kind: "uint64"
|
|
127
|
+
},
|
|
128
|
+
mediaTime: {
|
|
129
|
+
offset: 8,
|
|
130
|
+
kind: "int64"
|
|
131
|
+
},
|
|
132
|
+
mediaRate: {
|
|
133
|
+
offset: 16,
|
|
134
|
+
kind: "uint32"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* An edit's media rate is 16.16 fixed point; 1 plays the media at its own pace.
|
|
141
|
+
*/
|
|
142
|
+
var UNIT_MEDIA_RATE = 65536;
|
|
143
|
+
/**
|
|
144
|
+
* SampleToChunkBox, `stsc` (§8.7.4): from each 1-based first chunk on, how many samples a chunk
|
|
145
|
+
* holds, until the next entry's first chunk.
|
|
146
|
+
*/
|
|
147
|
+
var SAMPLE_TO_CHUNK = {
|
|
148
|
+
entrySize: 12,
|
|
149
|
+
fields: {
|
|
150
|
+
firstChunk: {
|
|
151
|
+
offset: 0,
|
|
152
|
+
kind: "uint32"
|
|
153
|
+
},
|
|
154
|
+
samplesPerChunk: {
|
|
155
|
+
offset: 4,
|
|
156
|
+
kind: "uint32"
|
|
157
|
+
},
|
|
158
|
+
sampleDescriptionIndex: {
|
|
159
|
+
offset: 8,
|
|
160
|
+
kind: "uint32"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* ChunkOffsetBox, `stco`, and ChunkLargeOffsetBox, `co64` (§8.7.5): where each chunk starts in the
|
|
166
|
+
* file, in 32 or 64 bits.
|
|
167
|
+
*/
|
|
168
|
+
var CHUNK_OFFSET = {
|
|
169
|
+
entrySize: 4,
|
|
170
|
+
fields: { chunkOffset: {
|
|
171
|
+
offset: 0,
|
|
172
|
+
kind: "uint32"
|
|
173
|
+
} }
|
|
174
|
+
};
|
|
175
|
+
var CHUNK_LARGE_OFFSET = {
|
|
176
|
+
entrySize: 8,
|
|
177
|
+
fields: { chunkOffset: {
|
|
178
|
+
offset: 0,
|
|
179
|
+
kind: "uint64"
|
|
180
|
+
} }
|
|
181
|
+
};
|
|
182
|
+
//#endregion
|
|
183
|
+
export { CHUNK_LARGE_OFFSET, CHUNK_OFFSET, COMPOSITION_OFFSET, EDIT_LIST, MEDIA_HEADER, MOVIE_HEADER, SAMPLE_TO_CHUNK, SYNC_SAMPLE, TIME_TO_SAMPLE, TRACK_HEADER_ID, UNIT_MEDIA_RATE };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { HandlerType, Mp4BoxType } from "./mp4BoxTypes.js";
|
|
2
|
+
import { keyframeRuleFor } from "./keyframeRules.js";
|
|
3
|
+
import { boxesIn, fullBoxOf, optionalBox, unreadableMovie } from "./movieBoxes.js";
|
|
4
|
+
import { SYNC_SAMPLE } from "./mp4Layouts.js";
|
|
5
|
+
import { readTableColumns } from "./readTable.js";
|
|
6
|
+
import { sampleLocationsOf } from "./sampleLocations.js";
|
|
7
|
+
import { movieTimescaleOf, trackBoxesOf, trackHeadersOf } from "./trackHeaders.js";
|
|
8
|
+
import { sampleTimingOf } from "./sampleTiming.js";
|
|
9
|
+
import { SampleTable } from "../../container/SampleTable.js";
|
|
10
|
+
import { TrackSampleTable } from "../../container/TrackSampleTable.js";
|
|
11
|
+
//#region ../../packages/core/src/domain/format/mp4/parseMovie.ts
|
|
12
|
+
/**
|
|
13
|
+
* The tracks the player plays, by handler; the others (time codes, metadata) are left out.
|
|
14
|
+
*/
|
|
15
|
+
var KIND_OF_HANDLER = {
|
|
16
|
+
[HandlerType.Video]: "video",
|
|
17
|
+
[HandlerType.Sound]: "audio"
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Sample numbers in the sync sample table count from one.
|
|
21
|
+
*/
|
|
22
|
+
var FIRST_SAMPLE_NUMBER = 1;
|
|
23
|
+
/**
|
|
24
|
+
* The sample table of a whole movie box (`moov`, header included): every sample of every track
|
|
25
|
+
* of picture or sound. A fragmented movie, whose samples are described in movie fragments after
|
|
26
|
+
* it, is refused: the cameras write none.
|
|
27
|
+
*/
|
|
28
|
+
function parseMovie(movieBox) {
|
|
29
|
+
const [movie] = boxesIn(movieBox);
|
|
30
|
+
if (movie?.type !== Mp4BoxType.Movie) throw unreadableMovie("is not where the file says it is");
|
|
31
|
+
const children = boxesIn(movie.body);
|
|
32
|
+
if (optionalBox(children, Mp4BoxType.MovieExtends)) throw unreadableMovie("is fragmented: its samples are described in movie fragments after it");
|
|
33
|
+
const movieTimescale = movieTimescaleOf(children);
|
|
34
|
+
const tracks = children.filter((box) => box.type === Mp4BoxType.Track).flatMap((box) => playedTrackOf(trackBoxesOf(box), movieTimescale));
|
|
35
|
+
return new SampleTable(tracks);
|
|
36
|
+
}
|
|
37
|
+
function playedTrackOf(boxes, movieTimescale) {
|
|
38
|
+
const headers = trackHeadersOf(boxes);
|
|
39
|
+
const kind = KIND_OF_HANDLER[headers.handlerType];
|
|
40
|
+
if (!kind) return [];
|
|
41
|
+
const locations = sampleLocationsOf(boxes.sampleTable);
|
|
42
|
+
const timing = sampleTimingOf(boxes, {
|
|
43
|
+
media: headers.mediaTimescale,
|
|
44
|
+
movie: movieTimescale,
|
|
45
|
+
sampleCount: locations.sizes.length
|
|
46
|
+
});
|
|
47
|
+
return [new TrackSampleTable({
|
|
48
|
+
trackId: headers.trackId,
|
|
49
|
+
kind,
|
|
50
|
+
...locations,
|
|
51
|
+
...timing,
|
|
52
|
+
syncSamples: syncSamplesOf(boxes.sampleTable),
|
|
53
|
+
keyframeRule: keyframeRuleFor(headers)
|
|
54
|
+
})];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The samples decoding may start at, counted from zero; undefined for a track without a sync
|
|
58
|
+
* sample table, whose every sample is one.
|
|
59
|
+
*/
|
|
60
|
+
function syncSamplesOf(sampleTable) {
|
|
61
|
+
const box = optionalBox(sampleTable, Mp4BoxType.SyncSample);
|
|
62
|
+
if (!box) return void 0;
|
|
63
|
+
const { sampleNumber } = readTableColumns(fullBoxOf(box).content, SYNC_SAMPLE);
|
|
64
|
+
return sampleNumber.map((number) => number - FIRST_SAMPLE_NUMBER);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { parseMovie };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { unreadableMovie } from "./movieBoxes.js";
|
|
2
|
+
//#region ../../packages/core/src/domain/format/mp4/readTable.ts
|
|
3
|
+
/**
|
|
4
|
+
* How each kind of integer in a box layout is read, big-endian.
|
|
5
|
+
*/
|
|
6
|
+
var READERS = {
|
|
7
|
+
uint32: (reader, offset) => reader.uint32BeAt(offset),
|
|
8
|
+
uint64: (reader, offset) => reader.uint64BeAt(offset),
|
|
9
|
+
int32: (reader, offset) => reader.int32BeAt(offset),
|
|
10
|
+
int64: (reader, offset) => reader.int64BeAt(offset)
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The integer a layout puts at `field`, counted from `base`.
|
|
14
|
+
*/
|
|
15
|
+
function readInteger(reader, field, base = 0) {
|
|
16
|
+
return READERS[field.kind](reader, base + field.offset);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A table box's entries, one column per field, read from its content after the version and
|
|
20
|
+
* flags.
|
|
21
|
+
*/
|
|
22
|
+
function readTableColumns(content, layout) {
|
|
23
|
+
const count = content.uint32BeAt(0);
|
|
24
|
+
if (4 + count * layout.entrySize > content.length) throw unreadableMovie(`has a table of ${count} entries that do not fit in its box`);
|
|
25
|
+
const fields = Object.keys(layout.fields);
|
|
26
|
+
const columns = {};
|
|
27
|
+
for (const field of fields) columns[field] = Array.from({ length: count }, (_, index) => readInteger(content, layout.fields[field], 4 + index * layout.entrySize));
|
|
28
|
+
return columns;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { readTableColumns };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Mp4BoxType } from "./mp4BoxTypes.js";
|
|
2
|
+
import { fullBoxOf, optionalBox, requiredBox, unreadableMovie } from "./movieBoxes.js";
|
|
3
|
+
import { CHUNK_LARGE_OFFSET, CHUNK_OFFSET, SAMPLE_TO_CHUNK } from "./mp4Layouts.js";
|
|
4
|
+
import { readTableColumns } from "./readTable.js";
|
|
5
|
+
//#region ../../packages/core/src/domain/format/mp4/sampleLocations.ts
|
|
6
|
+
/**
|
|
7
|
+
* Chunk numbers in the sample-to-chunk table count from one.
|
|
8
|
+
*/
|
|
9
|
+
var FIRST_CHUNK_NUMBER = 1;
|
|
10
|
+
/**
|
|
11
|
+
* The sample locations a sample table's boxes describe: the sizes of `stsz`, laid out one after
|
|
12
|
+
* another in the chunks `stsc` fills, which start where `stco` or `co64` says.
|
|
13
|
+
*/
|
|
14
|
+
function sampleLocationsOf(sampleTable) {
|
|
15
|
+
const sizes = sampleSizesOf(requiredBox(sampleTable, Mp4BoxType.SampleSize));
|
|
16
|
+
const chunkOffsets = chunkOffsetsOf(sampleTable);
|
|
17
|
+
const sampleToChunk = fullBoxOf(requiredBox(sampleTable, Mp4BoxType.SampleToChunk)).content;
|
|
18
|
+
return {
|
|
19
|
+
offsets: offsetsOf({
|
|
20
|
+
sizes,
|
|
21
|
+
chunkOffsets,
|
|
22
|
+
perChunk: samplesPerChunkOf(readTableColumns(sampleToChunk, SAMPLE_TO_CHUNK), chunkOffsets.length)
|
|
23
|
+
}),
|
|
24
|
+
sizes
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function sampleSizesOf(box) {
|
|
28
|
+
const { content } = fullBoxOf(box);
|
|
29
|
+
const shared = content.uint32BeAt(0);
|
|
30
|
+
const count = content.uint32BeAt(4);
|
|
31
|
+
if (shared !== 0) return new Float64Array(count).fill(shared);
|
|
32
|
+
if (8 + count * 4 > content.length) throw unreadableMovie(`lists ${count} sample sizes that do not fit in its box`);
|
|
33
|
+
return Float64Array.from({ length: count }, (_, index) => content.uint32BeAt(8 + index * 4));
|
|
34
|
+
}
|
|
35
|
+
function chunkOffsetsOf(sampleTable) {
|
|
36
|
+
const large = optionalBox(sampleTable, Mp4BoxType.ChunkLargeOffset);
|
|
37
|
+
const box = large ?? optionalBox(sampleTable, Mp4BoxType.ChunkOffset);
|
|
38
|
+
if (!box) throw unreadableMovie("has a sample table without chunk offsets");
|
|
39
|
+
const layout = large ? CHUNK_LARGE_OFFSET : CHUNK_OFFSET;
|
|
40
|
+
return readTableColumns(fullBoxOf(box).content, layout).chunkOffset;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* How many samples each chunk holds: every entry holds from its first chunk to the next entry's.
|
|
44
|
+
*/
|
|
45
|
+
function samplesPerChunkOf(table, chunkCount) {
|
|
46
|
+
const perChunk = Array.from({ length: chunkCount }, () => 0);
|
|
47
|
+
for (const [entry, firstChunk] of table.firstChunk.entries()) {
|
|
48
|
+
const nextFirst = table.firstChunk[entry + 1] ?? chunkCount + FIRST_CHUNK_NUMBER;
|
|
49
|
+
const count = table.samplesPerChunk[entry] ?? 0;
|
|
50
|
+
perChunk.fill(count, firstChunk - FIRST_CHUNK_NUMBER, nextFirst - FIRST_CHUNK_NUMBER);
|
|
51
|
+
}
|
|
52
|
+
return perChunk;
|
|
53
|
+
}
|
|
54
|
+
function offsetsOf(layout) {
|
|
55
|
+
const offsets = new Float64Array(layout.sizes.length);
|
|
56
|
+
let sample = 0;
|
|
57
|
+
for (const [chunk, chunkOffset] of layout.chunkOffsets.entries()) {
|
|
58
|
+
let offset = chunkOffset;
|
|
59
|
+
const last = Math.min(sample + (layout.perChunk[chunk] ?? 0), offsets.length);
|
|
60
|
+
for (; sample < last; sample += 1) {
|
|
61
|
+
offsets[sample] = offset;
|
|
62
|
+
offset += layout.sizes[sample] ?? 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (sample < offsets.length) throw unreadableMovie(`has chunks for ${sample} of its ${offsets.length} samples`);
|
|
66
|
+
return offsets;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { sampleLocationsOf };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { seconds } from "../../../shared/units/time.js";
|
|
2
|
+
import { Mp4BoxType } from "./mp4BoxTypes.js";
|
|
3
|
+
import { boxesIn, fullBoxOf, optionalBox, requiredBox, unreadableMovie } from "./movieBoxes.js";
|
|
4
|
+
import { COMPOSITION_OFFSET, EDIT_LIST, TIME_TO_SAMPLE } from "./mp4Layouts.js";
|
|
5
|
+
import { readTableColumns } from "./readTable.js";
|
|
6
|
+
import { inVersion } from "./trackHeaders.js";
|
|
7
|
+
import { presentationOrderOf } from "../../container/presentationOrder.js";
|
|
8
|
+
//#region ../../packages/core/src/domain/format/mp4/sampleTiming.ts
|
|
9
|
+
/**
|
|
10
|
+
* The track's timing as mediabunny reads it, so that both agree to the bit: a sample shows at its
|
|
11
|
+
* decode time plus its composition offset, less the edit list's shift, all in ticks, divided by
|
|
12
|
+
* the timescale once. Where composition offsets reorder the frames, each lasts until the next
|
|
13
|
+
* one shows and the last one shown for its own decode duration.
|
|
14
|
+
*/
|
|
15
|
+
function sampleTimingOf(boxes, context) {
|
|
16
|
+
const decode = decodeTicksOf(requiredBox(boxes.sampleTable, Mp4BoxType.TimeToSample), context);
|
|
17
|
+
const offsets = compositionOffsetsOf(boxes.sampleTable, context.sampleCount);
|
|
18
|
+
const presentation = offsets ? decode.times.map((time, sample) => time + (offsets[sample] ?? 0)) : decode.times;
|
|
19
|
+
const shift = editShiftOf(boxes.track, context);
|
|
20
|
+
const timestamps = presentation.map((ticks) => (ticks - shift) / context.media);
|
|
21
|
+
const durations = (offsets ? presentationGaps(presentation, decode.deltas) : decode.deltas).map((ticks) => ticks / context.media);
|
|
22
|
+
return {
|
|
23
|
+
timestamps,
|
|
24
|
+
durations,
|
|
25
|
+
end: endOf(timestamps, durations, context.media)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Each sample's decode time and duration, from the runs of `stts`.
|
|
30
|
+
*/
|
|
31
|
+
function decodeTicksOf(box, context) {
|
|
32
|
+
const runs = readTableColumns(fullBoxOf(box).content, TIME_TO_SAMPLE);
|
|
33
|
+
const deltas = expandedRuns(runs.sampleCount, runs.sampleDelta);
|
|
34
|
+
if (deltas.length !== context.sampleCount) throw unreadableMovie(`times ${deltas.length} samples of a track that has ${context.sampleCount}`);
|
|
35
|
+
const times = new Float64Array(deltas.length);
|
|
36
|
+
for (let sample = 1; sample < deltas.length; sample += 1) times[sample] = (times[sample - 1] ?? 0) + (deltas[sample - 1] ?? 0);
|
|
37
|
+
return {
|
|
38
|
+
times,
|
|
39
|
+
deltas
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Each sample's composition offset from the runs of `ctts`, zero past its last run; undefined
|
|
44
|
+
* for a track without the box, whose frames show in decode order.
|
|
45
|
+
*/
|
|
46
|
+
function compositionOffsetsOf(sampleTable, sampleCount) {
|
|
47
|
+
const box = optionalBox(sampleTable, Mp4BoxType.CompositionOffset);
|
|
48
|
+
if (!box) return void 0;
|
|
49
|
+
const runs = readTableColumns(fullBoxOf(box).content, COMPOSITION_OFFSET);
|
|
50
|
+
const offsets = new Float64Array(sampleCount);
|
|
51
|
+
offsets.set(expandedRuns(runs.sampleCount, runs.sampleOffset).subarray(0, sampleCount));
|
|
52
|
+
return offsets;
|
|
53
|
+
}
|
|
54
|
+
function expandedRuns(counts, values) {
|
|
55
|
+
const total = counts.reduce((sum, count) => sum + count, 0);
|
|
56
|
+
const expanded = new Float64Array(total);
|
|
57
|
+
let start = 0;
|
|
58
|
+
for (const [run, count] of counts.entries()) {
|
|
59
|
+
expanded.fill(values[run] ?? 0, start, start + count);
|
|
60
|
+
start += count;
|
|
61
|
+
}
|
|
62
|
+
return expanded;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The ticks the edit list moves the media by: the media time of its first edit that shows media
|
|
66
|
+
* at the media's own pace, less the empty edits before it (movie time, rounded to media ticks).
|
|
67
|
+
* Later edits are not followed, as mediabunny does not follow them.
|
|
68
|
+
*/
|
|
69
|
+
function editShiftOf(track, context) {
|
|
70
|
+
const edit = optionalBox(track, Mp4BoxType.Edit);
|
|
71
|
+
const list = edit && optionalBox(boxesIn(edit.body), Mp4BoxType.EditList);
|
|
72
|
+
if (!list) return 0;
|
|
73
|
+
const box = fullBoxOf(list);
|
|
74
|
+
const entries = readTableColumns(box.content, inVersion(box, EDIT_LIST, Mp4BoxType.EditList));
|
|
75
|
+
let emptyDuration = 0;
|
|
76
|
+
for (const [entry, mediaTime] of entries.mediaTime.entries()) {
|
|
77
|
+
if (mediaTime === -1) {
|
|
78
|
+
emptyDuration += entries.segmentDuration[entry] ?? 0;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
return entries.mediaRate[entry] === 65536 ? mediaTime - Math.round(emptyDuration / context.movie * context.media) : 0;
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How long each sample shows: until the next one in presentation order (ties kept in decode
|
|
87
|
+
* order), the last one for its own decode duration.
|
|
88
|
+
*/
|
|
89
|
+
function presentationGaps(presentation, deltas) {
|
|
90
|
+
const order = presentationOrderOf(presentation);
|
|
91
|
+
const gaps = Float64Array.from(deltas);
|
|
92
|
+
for (let position = 0; position < order.length - 1; position += 1) {
|
|
93
|
+
const sample = order[position] ?? 0;
|
|
94
|
+
gaps[sample] = (presentation[order[position + 1] ?? 0] ?? 0) - (presentation[sample] ?? 0);
|
|
95
|
+
}
|
|
96
|
+
return gaps;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* When the last sample shown stops showing, rounded to the timescale as mediabunny rounds a
|
|
100
|
+
* track's duration.
|
|
101
|
+
*/
|
|
102
|
+
function endOf(timestamps, durations, timescale) {
|
|
103
|
+
const last = presentationOrderOf(timestamps).at(-1);
|
|
104
|
+
if (last === void 0) return seconds(0);
|
|
105
|
+
const end = (timestamps[last] ?? 0) + (durations[last] ?? 0);
|
|
106
|
+
return seconds(Math.round(end * timescale) / timescale);
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { sampleTimingOf };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HandlerType, Mp4BoxType } from "./mp4BoxTypes.js";
|
|
2
|
+
import { boxesIn, fullBoxOf, requiredBox, unreadableMovie } from "./movieBoxes.js";
|
|
3
|
+
import { MEDIA_HEADER, MOVIE_HEADER, TRACK_HEADER_ID } from "./mp4Layouts.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/format/mp4/trackHeaders.ts
|
|
5
|
+
var FOUR_CHARACTERS = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Where the length size of each codec family's NAL units is kept, by its configuration box.
|
|
8
|
+
*/
|
|
9
|
+
var LENGTH_SIZE_OFFSETS = /* @__PURE__ */ new Map([[Mp4BoxType.AvcConfiguration, 4], [Mp4BoxType.HevcConfiguration, 21]]);
|
|
10
|
+
/**
|
|
11
|
+
* The timescale of the movie header, in which edit lists count.
|
|
12
|
+
*/
|
|
13
|
+
function movieTimescaleOf(movie) {
|
|
14
|
+
const header = fullBoxOf(requiredBox(movie, Mp4BoxType.MovieHeader));
|
|
15
|
+
return header.content.uint32BeAt(inVersion(header, MOVIE_HEADER, Mp4BoxType.MovieHeader).timescale);
|
|
16
|
+
}
|
|
17
|
+
function trackBoxesOf(trackBox) {
|
|
18
|
+
const track = boxesIn(trackBox.body);
|
|
19
|
+
const media = boxesIn(requiredBox(track, Mp4BoxType.Media).body);
|
|
20
|
+
const information = boxesIn(requiredBox(media, Mp4BoxType.MediaInformation).body);
|
|
21
|
+
return {
|
|
22
|
+
track,
|
|
23
|
+
media,
|
|
24
|
+
sampleTable: boxesIn(requiredBox(information, Mp4BoxType.SampleTable).body)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function trackHeadersOf(boxes) {
|
|
28
|
+
const handlerType = handlerTypeOf(boxes.media);
|
|
29
|
+
const entry = firstSampleEntryOf(boxes.sampleTable);
|
|
30
|
+
return {
|
|
31
|
+
trackId: trackIdOf(boxes.track),
|
|
32
|
+
handlerType,
|
|
33
|
+
mediaTimescale: mediaTimescaleOf(boxes.media),
|
|
34
|
+
sampleEntryType: entry.type,
|
|
35
|
+
nalLengthSize: handlerType === HandlerType.Video ? nalLengthSizeOf(entry) : void 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The layout of a box in its version; a version the standard does not define is refused.
|
|
40
|
+
*/
|
|
41
|
+
function inVersion(box, layouts, type) {
|
|
42
|
+
const layout = box.version === 0 || box.version === 1 ? layouts[box.version] : void 0;
|
|
43
|
+
if (layout === void 0) throw unreadableMovie(`has a ${type} box of version ${box.version}`);
|
|
44
|
+
return layout;
|
|
45
|
+
}
|
|
46
|
+
function trackIdOf(track) {
|
|
47
|
+
const header = fullBoxOf(requiredBox(track, Mp4BoxType.TrackHeader));
|
|
48
|
+
return header.content.uint32BeAt(inVersion(header, TRACK_HEADER_ID, Mp4BoxType.TrackHeader));
|
|
49
|
+
}
|
|
50
|
+
function mediaTimescaleOf(media) {
|
|
51
|
+
const header = fullBoxOf(requiredBox(media, Mp4BoxType.MediaHeader));
|
|
52
|
+
return header.content.uint32BeAt(inVersion(header, MEDIA_HEADER, Mp4BoxType.MediaHeader).timescale);
|
|
53
|
+
}
|
|
54
|
+
function handlerTypeOf(media) {
|
|
55
|
+
const { content } = fullBoxOf(requiredBox(media, Mp4BoxType.Handler));
|
|
56
|
+
return content.asciiAt(4, FOUR_CHARACTERS);
|
|
57
|
+
}
|
|
58
|
+
function firstSampleEntryOf(sampleTable) {
|
|
59
|
+
const { content } = fullBoxOf(requiredBox(sampleTable, Mp4BoxType.SampleDescription));
|
|
60
|
+
const entriesLength = content.length - 4;
|
|
61
|
+
const [entry] = boxesIn(content.bytesAt(4, entriesLength));
|
|
62
|
+
if (!entry) throw unreadableMovie("has a track without a sample entry");
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The bytes before each NAL unit that give its length, from the codec configuration inside the
|
|
67
|
+
* visual sample entry; undefined for a codec without one (the codec reader then refuses it).
|
|
68
|
+
*/
|
|
69
|
+
function nalLengthSizeOf(entry) {
|
|
70
|
+
const configuration = boxesIn(entry.body.subarray(78)).find((box) => LENGTH_SIZE_OFFSETS.has(box.type));
|
|
71
|
+
const byte = configuration?.body[LENGTH_SIZE_OFFSETS.get(configuration.type) ?? 0];
|
|
72
|
+
return byte === void 0 ? void 0 : (byte & 3) + 1;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { inVersion, movieTimescaleOf, trackBoxesOf, trackHeadersOf };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { degrees } from "./shared/units/angle.js";
|
|
2
2
|
import "./domain/optics/EquidistantModel.js";
|
|
3
3
|
import "./domain/format/calibration/CalibrationVersion.js";
|
|
4
|
-
import { GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf } from "./shared/errors/GyroViewError.js";
|
|
4
|
+
import { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf } from "./shared/errors/GyroViewError.js";
|
|
5
5
|
import { lazy } from "./shared/lazy.js";
|
|
6
6
|
import { microseconds, microsecondsToSeconds, milliseconds, seconds, secondsToMicroseconds } from "./shared/units/time.js";
|
|
7
7
|
import "./domain/motion/exposure/ExposureRecord.js";
|
|
@@ -21,6 +21,9 @@ import { timeRecording } from "./application/recording/timeRecording.js";
|
|
|
21
21
|
import "./domain/format/naming/RecordingFileName.js";
|
|
22
22
|
import { fileNameOfUrl } from "./shared/text/urlPath.js";
|
|
23
23
|
import { locateOtherLensFile } from "./application/recording/locateOtherLensFile.js";
|
|
24
|
+
import "./domain/container/SampleTable.js";
|
|
25
|
+
import "./domain/container/TrackSampleTable.js";
|
|
26
|
+
import { readSampleTable } from "./application/recording/readSampleTable.js";
|
|
24
27
|
import "./domain/stitching/LensLayout.js";
|
|
25
28
|
import { detectLensLayout } from "./domain/format/layout/detectLensLayout.js";
|
|
26
29
|
import { DEFAULT_STABILIZATION_MODE, STABILIZATION_MODES } from "./domain/motion/stabilization/Stabilizer.js";
|
|
@@ -47,6 +50,11 @@ import "./domain/stitching/seamDisparityField.js";
|
|
|
47
50
|
import "./domain/stitching/seamJoin.js";
|
|
48
51
|
import { keysOf } from "./shared/keysOf.js";
|
|
49
52
|
import { Outbox } from "./shared/events/Outbox.js";
|
|
53
|
+
import { Ending, ITERATION_END } from "./shared/async/iteration.js";
|
|
54
|
+
import { SourceByteStream } from "./application/download/SourceByteStream.js";
|
|
55
|
+
import { startFileDownload } from "./application/download/startFileDownload.js";
|
|
56
|
+
import { downloadPolicyFor } from "./domain/download/DownloadPolicy.js";
|
|
57
|
+
import { RecordingBuffer } from "./application/download/RecordingBuffer.js";
|
|
50
58
|
import "./application/playback/FramePairQueue.js";
|
|
51
59
|
import { Deferred } from "./shared/async/Deferred.js";
|
|
52
60
|
import { RunStop, STOPPED } from "./shared/async/RunStop.js";
|
|
@@ -56,4 +64,4 @@ import { WallClock } from "./application/playback/WallClock.js";
|
|
|
56
64
|
import { isFlowing } from "./domain/playback/PlayerState.js";
|
|
57
65
|
import { TypedEmitter } from "./shared/events/TypedEmitter.js";
|
|
58
66
|
import { PlaybackSession } from "./application/playback/PlaybackSession.js";
|
|
59
|
-
export { ByteRange, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, IDENTITY_MATRIX3, Outbox, PICTURE_QUALITIES, PlaybackSession, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, StabilizingFrameSink, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, messageOf, microseconds, microsecondsToSeconds, milliseconds, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, seconds, secondsToMicroseconds, stabilizerFor, timeRecording, viewModeRulesFor, zoomStepsForPinch };
|
|
67
|
+
export { ByteRange, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, IDENTITY_MATRIX3, ITERATION_END, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, messageOf, microseconds, microsecondsToSeconds, milliseconds, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region ../../packages/core/src/shared/async/iteration.ts
|
|
2
|
+
/**
|
|
3
|
+
* What an iteration answers once it has ended.
|
|
4
|
+
*/
|
|
5
|
+
var ITERATION_END = {
|
|
6
|
+
done: true,
|
|
7
|
+
value: void 0
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Whether an iteration has ended, asked by a call after every wait: TypeScript takes a field read
|
|
11
|
+
* before an `await` for the same value after it, though a `return()` may have ended the iteration
|
|
12
|
+
* meanwhile.
|
|
13
|
+
*/
|
|
14
|
+
var Ending = class {
|
|
15
|
+
isEnded = false;
|
|
16
|
+
end() {
|
|
17
|
+
this.isEnded = true;
|
|
18
|
+
}
|
|
19
|
+
hasEnded() {
|
|
20
|
+
return this.isEnded;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Ending, ITERATION_END };
|
|
@@ -26,6 +26,18 @@ var ByteRange = class ByteRange {
|
|
|
26
26
|
return this.offset + this.length;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
+
* Whether the two share a byte; an empty range shares none.
|
|
30
|
+
*/
|
|
31
|
+
overlaps(other) {
|
|
32
|
+
return Math.max(this.offset, other.offset) < Math.min(this.end, other.end);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether `other` lies wholly within this range.
|
|
36
|
+
*/
|
|
37
|
+
contains(other) {
|
|
38
|
+
return this.offset <= other.offset && other.end <= this.end;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
29
41
|
* Refuses a range that runs past the end of a `totalSize`-byte source with the error the
|
|
30
42
|
* `RandomAccessSource` contract asks for; `source` names the source in the message.
|
|
31
43
|
*/
|