@bubo-squared/gyroview 0.1.0 → 0.3.0
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 +60 -0
- package/README.md +21 -5
- package/dist/index.d.ts +24 -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 +15 -4
- 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/three/src/shaders/rawLenses.frag.js +1 -1
- 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 +15 -1
- 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/domain/view/lensTilesView.js +2 -2
- package/dist/packages/core/src/domain/view/magnification.js +1 -1
- package/dist/packages/core/src/domain/view/panoramaView.js +1 -1
- package/dist/packages/core/src/index.js +9 -1
- 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 +1 -0
- package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
- package/dist/packages/player/src/composition/browserPorts.js +38 -5
- package/dist/packages/player/src/composition/buildPipeline.js +15 -3
- package/dist/packages/player/src/composition/openInputs.js +51 -109
- package/dist/packages/player/src/composition/openRecording.js +13 -2
- package/dist/packages/player/src/composition/readInputs.js +111 -0
- package/dist/packages/player/src/controls/ChoiceMenu.js +16 -4
- package/dist/packages/player/src/controls/PictureMenus.js +22 -8
- package/dist/packages/player/src/controls/SeekBar.js +6 -3
- package/dist/packages/player/src/controls/SoundControls.js +2 -0
- package/dist/packages/player/src/controls/TransportButtons.js +2 -5
- package/dist/packages/player/src/controls/Wording.js +11 -9
- package/dist/packages/player/src/controls/controlParts.js +5 -3
- package/dist/packages/player/src/controls/controls.js +1 -1
- package/dist/packages/player/src/controls/controlsBar.js +4 -1
- package/dist/packages/player/src/controls/controlsMarkup.js +36 -21
- package/dist/packages/player/src/controls/icons.js +32 -8
- package/dist/packages/player/src/controls/messages.js +27 -4
- package/dist/packages/player/src/controls/sliderFill.js +16 -0
- package/dist/packages/player/src/element/ElementLoads.js +7 -0
- package/dist/packages/player/src/element/GyroViewElement.js +8 -3
- package/dist/packages/player/src/element/attributeNames.js +12 -1
- package/dist/packages/player/src/element/attributes.js +29 -7
- package/dist/packages/player/src/element/reflectedProperties.js +19 -1
- package/dist/packages/player/src/element/styles.js +1 -1
- package/dist/packages/player/src/inspectRecording.js +1 -1
- package/dist/standalone.js +385 -126
- 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,175 @@
|
|
|
1
|
+
import { seconds } from "../../shared/units/time.js";
|
|
2
|
+
import { ByteRange } from "../../shared/binary/ByteRange.js";
|
|
3
|
+
import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
|
|
4
|
+
import { needsOf, withinBytes } from "./trackNeeds.js";
|
|
5
|
+
import { resumeThresholdOf } from "./isReadyToResume.js";
|
|
6
|
+
//#region ../../packages/core/src/domain/download/planDownloads.ts
|
|
7
|
+
/**
|
|
8
|
+
* The download's plan, from where its readers stand (ADR 0029). The window starts at the
|
|
9
|
+
* slowest picture reader, where the picture last stood once its readers closed (so the sound
|
|
10
|
+
* after the last frame still comes), or at the sound where no picture was ever read; once
|
|
11
|
+
* playing it reaches as far ahead as the policy's seconds and bytes allow, before that only to
|
|
12
|
+
* what the readers wait for. Every picture reader's next frame is read, however far from the
|
|
13
|
+
* others, so none waits for ever; a reader a seek left behind wants nothing until it follows.
|
|
14
|
+
* What is wanted is asked for lowest first and in large ranges: at once when playback needs it
|
|
15
|
+
* now, otherwise once a refill's worth is missing.
|
|
16
|
+
*/
|
|
17
|
+
function planDownloads(state) {
|
|
18
|
+
const pictures = state.cursors.filter((cursor) => isPicture(cursor));
|
|
19
|
+
const anchor = pictures.length > 0 ? anchorOf(pictures) : state.anchor;
|
|
20
|
+
const windowStart = anchor ?? soundLeadOf(state);
|
|
21
|
+
if (!windowStart) return nothingToRead(state);
|
|
22
|
+
const ahead = state.isReadingAhead ? state.policy.aheadSeconds : 0;
|
|
23
|
+
const windowEnd = seconds(windowStart.time + ahead);
|
|
24
|
+
const wanted = wantedBytes({
|
|
25
|
+
state,
|
|
26
|
+
windowStart,
|
|
27
|
+
windowEnd
|
|
28
|
+
}).bridgingGapsBelow(state.policy.bridgedGap);
|
|
29
|
+
const kept = state.transfers.filter((transfer) => transfer.remaining.length === 0 || wanted.overlaps(transfer.remaining));
|
|
30
|
+
const coming = ByteRangeSet.of(kept.map((transfer) => transfer.remaining));
|
|
31
|
+
const missing = wanted.subtract(state.held).subtract(coming).subtract(state.unreadable);
|
|
32
|
+
const room = state.policy.requestsInFlight - kept.length;
|
|
33
|
+
return {
|
|
34
|
+
start: isTopUpDue({
|
|
35
|
+
state,
|
|
36
|
+
windowStart,
|
|
37
|
+
windowEnd,
|
|
38
|
+
missing,
|
|
39
|
+
threshold: thresholdOf(state, windowStart)
|
|
40
|
+
}) ? requestsFor(missing, state.policy, room) : [],
|
|
41
|
+
cancel: state.transfers.filter((transfer) => !kept.includes(transfer)).map((transfer) => transfer.id),
|
|
42
|
+
release: state.held.subtract(wanted.union(keptBehind(windowStart, state.policy))),
|
|
43
|
+
anchor
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Where the window starts once playing where no picture was ever read: at the sound, which then
|
|
48
|
+
* has no picture to follow, and follows it as it moves on.
|
|
49
|
+
*/
|
|
50
|
+
function soundLeadOf(state) {
|
|
51
|
+
return state.isReadingAhead && state.cursors.length > 0 ? anchorOf(state.cursors) : void 0;
|
|
52
|
+
}
|
|
53
|
+
function nothingToRead(state) {
|
|
54
|
+
return {
|
|
55
|
+
start: [],
|
|
56
|
+
cancel: state.transfers.map((transfer) => transfer.id),
|
|
57
|
+
release: ByteRangeSet.empty,
|
|
58
|
+
anchor: void 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function anchorOf(cursors) {
|
|
62
|
+
return {
|
|
63
|
+
time: seconds(Math.min(...cursors.map((cursor) => timeOf(cursor)))),
|
|
64
|
+
offset: Math.min(...cursors.map((cursor) => offsetOf(cursor))),
|
|
65
|
+
keyframeOffset: Math.min(...cursors.map((cursor) => keyframeOffsetOf(cursor)))
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every sample the readers following the picture will hand out within the window, clipped to
|
|
70
|
+
* the bytes the policy allows ahead, nearest first; and the next sample of every picture reader
|
|
71
|
+
* and of every reader waiting within the window, whatever the budget and wherever the file
|
|
72
|
+
* puts it, so no reader waits for ever.
|
|
73
|
+
*/
|
|
74
|
+
function wantedBytes(window) {
|
|
75
|
+
const { state, windowEnd } = window;
|
|
76
|
+
const following = state.cursors.filter((cursor) => isFollowing(cursor, window));
|
|
77
|
+
const needs = following.flatMap((cursor) => needsOf(cursor, laterOf(windowEnd, cursor.target)));
|
|
78
|
+
const next = following.filter((cursor) => isNextSampleOwed(cursor, windowEnd));
|
|
79
|
+
const nextSamples = ByteRangeSet.of(next.map((cursor) => cursor.track.rangeOf(cursor.sample)));
|
|
80
|
+
return withinBytes(ByteRangeSet.of(needs), state.policy.aheadBytes).union(nextSamples);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A reader no further behind the picture than the policy allows: the sound lags the picture
|
|
84
|
+
* by the decoders' lead, and until it follows a seek it stands far behind.
|
|
85
|
+
*/
|
|
86
|
+
function isFollowing(cursor, window) {
|
|
87
|
+
return timeOf(cursor) >= window.windowStart.time - window.state.policy.keepBehindSeconds;
|
|
88
|
+
}
|
|
89
|
+
function isNextSampleOwed(cursor, windowEnd) {
|
|
90
|
+
const isWaitingWithin = cursor.isWaiting && timeOf(cursor) <= windowEnd;
|
|
91
|
+
return !isAtItsEnd(cursor) && (isPicture(cursor) || isWaitingWithin);
|
|
92
|
+
}
|
|
93
|
+
function laterOf(time, other) {
|
|
94
|
+
return other !== void 0 && other > time ? other : time;
|
|
95
|
+
}
|
|
96
|
+
function isPicture(cursor) {
|
|
97
|
+
return cursor.track.kind === "video";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Once playing, what playback that starved would wait for, which the download keeps held; none
|
|
101
|
+
* before.
|
|
102
|
+
*/
|
|
103
|
+
function thresholdOf(state, windowStart) {
|
|
104
|
+
const need = {
|
|
105
|
+
cursors: state.cursors,
|
|
106
|
+
time: windowStart.time,
|
|
107
|
+
policy: state.policy
|
|
108
|
+
};
|
|
109
|
+
return state.isReadingAhead ? resumeThresholdOf(need) : ByteRangeSet.empty;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether to ask for what is missing now: a reader needs it at once, the resume threshold is
|
|
113
|
+
* not all held, or a refill's worth is missing. The last seconds of the tracks come as the
|
|
114
|
+
* threshold reaches them, however few their bytes.
|
|
115
|
+
*/
|
|
116
|
+
function isTopUpDue(topUp) {
|
|
117
|
+
const { state, missing, threshold } = topUp;
|
|
118
|
+
if (missing.ranges.some((range) => threshold.overlaps(range))) return true;
|
|
119
|
+
return state.cursors.some((cursor) => isNeededAtOnce(cursor, topUp) && missing.overlaps(cursor.track.rangeOf(cursor.sample))) || missing.totalLength >= state.policy.refillBytes;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A reader waiting on a sample playback needs now: a picture's, or one due within the resume
|
|
123
|
+
* seconds. Sound that buffers far ahead waits where the downloaded bytes end, and would
|
|
124
|
+
* otherwise ask for a small range every time the picture moved.
|
|
125
|
+
*/
|
|
126
|
+
function isNeededAtOnce(cursor, topUp) {
|
|
127
|
+
if (!cursor.isWaiting || isAtItsEnd(cursor)) return false;
|
|
128
|
+
const { windowStart, state } = topUp;
|
|
129
|
+
const horizon = windowStart.time + (state.isReadingAhead ? state.policy.resumeSeconds : 0);
|
|
130
|
+
return isPicture(cursor) || timeOf(cursor) <= horizon;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The missing ranges in order, cut to the request size, as many as there is room for.
|
|
134
|
+
*/
|
|
135
|
+
function requestsFor(missing, policy, room) {
|
|
136
|
+
const requests = [];
|
|
137
|
+
for (const range of missing.ranges) for (let offset = range.offset; offset < range.end && requests.length < room; offset += policy.requestSize) requests.push(ByteRange.of(offset, Math.min(policy.requestSize, range.end - offset)));
|
|
138
|
+
return requests;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The stretch behind the picture that is kept for short seeks back: its group of pictures from
|
|
142
|
+
* the key frame, or the policy's distance, whichever reaches further back.
|
|
143
|
+
*/
|
|
144
|
+
function keptBehind(anchor, policy) {
|
|
145
|
+
const start = keptBehindStart(anchor, policy);
|
|
146
|
+
return ByteRangeSet.of([ByteRange.of(start, anchor.offset - start)]);
|
|
147
|
+
}
|
|
148
|
+
function keptBehindStart(anchor, policy) {
|
|
149
|
+
return Math.max(0, Math.min(anchor.keyframeOffset, anchor.offset - policy.keepBehindBytes));
|
|
150
|
+
}
|
|
151
|
+
function isAtItsEnd(cursor) {
|
|
152
|
+
return cursor.sample >= cursor.track.sampleCount;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* When a picture reader stands: at its next frame, or at its track's end once it read them all.
|
|
156
|
+
*/
|
|
157
|
+
function timeOf(cursor) {
|
|
158
|
+
return isAtItsEnd(cursor) ? cursor.track.end : cursor.track.timestampOf(cursor.sample);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Where the key frame before a reader's next sample lies; a sound sample is its own.
|
|
162
|
+
*/
|
|
163
|
+
function keyframeOffsetOf(cursor) {
|
|
164
|
+
const { track } = cursor;
|
|
165
|
+
const last = Math.min(cursor.sample, track.sampleCount - 1);
|
|
166
|
+
const keyframe = track.syncSampleAtOrBefore(last);
|
|
167
|
+
return keyframe === void 0 ? offsetOf(cursor) : track.rangeOf(keyframe).offset;
|
|
168
|
+
}
|
|
169
|
+
function offsetOf(cursor) {
|
|
170
|
+
const { track } = cursor;
|
|
171
|
+
if (!isAtItsEnd(cursor)) return track.rangeOf(cursor.sample).offset;
|
|
172
|
+
return track.sampleCount > 0 ? track.rangeOf(track.sampleCount - 1).end : 0;
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { planDownloads };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ByteRange } from "../../shared/binary/ByteRange.js";
|
|
2
|
+
import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/download/trackNeeds.ts
|
|
4
|
+
/**
|
|
5
|
+
* The ranges of the samples a cursor will hand out, in decode order, up to the first that shows
|
|
6
|
+
* after `until`: with B-frames the window ends up to a reordering group early, which the plans
|
|
7
|
+
* after read.
|
|
8
|
+
*/
|
|
9
|
+
function needsOf(cursor, until) {
|
|
10
|
+
const { track } = cursor;
|
|
11
|
+
const needs = [];
|
|
12
|
+
for (let sample = cursor.sample; sample < track.sampleCount; sample += 1) {
|
|
13
|
+
if (track.timestampOf(sample) > until) break;
|
|
14
|
+
needs.push(track.rangeOf(sample));
|
|
15
|
+
}
|
|
16
|
+
return needs;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The first `budget` bytes of `wanted`, lowest first; the gaps between its ranges cost nothing.
|
|
20
|
+
*/
|
|
21
|
+
function withinBytes(wanted, budget) {
|
|
22
|
+
const kept = [];
|
|
23
|
+
let left = budget;
|
|
24
|
+
for (const range of wanted.ranges) {
|
|
25
|
+
if (left <= 0) break;
|
|
26
|
+
kept.push(ByteRange.of(range.offset, Math.min(range.length, left)));
|
|
27
|
+
left -= range.length;
|
|
28
|
+
}
|
|
29
|
+
return ByteRangeSet.of(kept);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { needsOf, withinBytes };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { hasErrorCode } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { isPlausibleBoxType } from "./boxConstants.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/format/boxes/boxHeader.ts
|
|
4
|
+
/**
|
|
5
|
+
* The header of the box whose first bytes `header` holds, in a space of `room` bytes from the
|
|
6
|
+
* box's start (to the end of the file, or of the box around it), which a size of zero fills.
|
|
7
|
+
* Undefined when those bytes form no box that fits there: the file's boxes have ended and a
|
|
8
|
+
* bare trailer begins, or a box inside another is malformed.
|
|
9
|
+
*/
|
|
10
|
+
function boxHeaderOf(header, room) {
|
|
11
|
+
if (header.length < 8) return void 0;
|
|
12
|
+
const type = header.asciiAt(4, 4);
|
|
13
|
+
if (!isPlausibleBoxType(type)) return void 0;
|
|
14
|
+
const extent = extentOf(header, room);
|
|
15
|
+
return extent !== void 0 && extent.size >= extent.headerSize && extent.size <= room ? {
|
|
16
|
+
type,
|
|
17
|
+
...extent
|
|
18
|
+
} : void 0;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Undefined when the header declares a large size that is not a safe integer: real boxes never
|
|
22
|
+
* do, so those bytes are not a box.
|
|
23
|
+
*/
|
|
24
|
+
function extentOf(header, room) {
|
|
25
|
+
const declared = header.uint32BeAt(0);
|
|
26
|
+
if (declared === 1 && header.length >= 16) return largeExtent(header);
|
|
27
|
+
return {
|
|
28
|
+
size: declared === 0 ? room : declared,
|
|
29
|
+
headerSize: 8
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function largeExtent(header) {
|
|
33
|
+
try {
|
|
34
|
+
return {
|
|
35
|
+
size: header.uint64BeAt(8),
|
|
36
|
+
headerSize: 16
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (hasErrorCode(error, "binary-unsafe-integer")) return void 0;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { boxHeaderOf };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { hasErrorCode } from "../../../shared/errors/GyroViewError.js";
|
|
2
1
|
import { ByteReader } from "../../../shared/binary/ByteReader.js";
|
|
3
2
|
import { ByteRange } from "../../../shared/binary/ByteRange.js";
|
|
4
|
-
import {
|
|
3
|
+
import { boxHeaderOf } from "./boxHeader.js";
|
|
5
4
|
//#region ../../packages/core/src/domain/format/boxes/scanBoxes.ts
|
|
6
5
|
/**
|
|
7
6
|
* Walks the top-level boxes of a file with one small read per box, and stops at the first bytes
|
|
@@ -20,40 +19,14 @@ async function scanBoxes(source, fileSize) {
|
|
|
20
19
|
return boxes;
|
|
21
20
|
}
|
|
22
21
|
async function readBoxAt(source, offset, fileSize) {
|
|
23
|
-
const
|
|
22
|
+
const room = fileSize - offset;
|
|
23
|
+
const headerLength = Math.min(16, room);
|
|
24
24
|
const header = new ByteReader(await source.read(ByteRange.of(offset, headerLength)));
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const { size, headerSize } = resolved;
|
|
30
|
-
return size >= headerSize && offset + size <= fileSize ? {
|
|
31
|
-
type,
|
|
32
|
-
range: ByteRange.of(offset, size)
|
|
33
|
-
} : void 0;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Undefined when the header declares a large size that is not a safe integer: real boxes never
|
|
37
|
-
* do, so those bytes are trailer data, not a box.
|
|
38
|
-
*/
|
|
39
|
-
function resolveSize(header, offset, fileSize) {
|
|
40
|
-
const declared = header.uint32BeAt(0);
|
|
41
|
-
if (declared === 1 && header.length >= 16) return largeSize(header);
|
|
42
|
-
return {
|
|
43
|
-
size: declared === 0 ? fileSize - offset : declared,
|
|
44
|
-
headerSize: 8
|
|
25
|
+
const box = boxHeaderOf(header, room);
|
|
26
|
+
return box && {
|
|
27
|
+
type: box.type,
|
|
28
|
+
range: ByteRange.of(offset, box.size)
|
|
45
29
|
};
|
|
46
30
|
}
|
|
47
|
-
function largeSize(header) {
|
|
48
|
-
try {
|
|
49
|
-
return {
|
|
50
|
-
size: header.uint64BeAt(8),
|
|
51
|
-
headerSize: 16
|
|
52
|
-
};
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (hasErrorCode(error, "binary-unsafe-integer")) return void 0;
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
31
|
//#endregion
|
|
59
32
|
export { scanBoxes };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AVC_SAMPLE_ENTRIES, HEVC_SAMPLE_ENTRIES } from "./mp4BoxTypes.js";
|
|
2
|
+
import { EVERY_SAMPLE_IS_A_KEYFRAME } from "../../container/KeyframeRule.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/format/mp4/keyframeRules.ts
|
|
4
|
+
/**
|
|
5
|
+
* ITU-T H.264 Table 7-1: nal_unit_type is the low five bits of the header byte; types 1 to 4 are
|
|
6
|
+
* slices of a picture that is not an IDR picture, type 5 a slice of one.
|
|
7
|
+
*/
|
|
8
|
+
var AVC_TYPE_MASK = 31;
|
|
9
|
+
var AVC_FIRST_NON_IDR_SLICE = 1;
|
|
10
|
+
var AVC_LAST_SLICE_PARTITION = 4;
|
|
11
|
+
var AVC_IDR_SLICE = 5;
|
|
12
|
+
/**
|
|
13
|
+
* ITU-T H.265 Table 7-1: nal_unit_type is six bits after the header's first bit; types 0 to 15
|
|
14
|
+
* are pictures that are no intra random access point, 16 (BLA_W_LP) to 23 (RSV_IRAP_VCL23) ones
|
|
15
|
+
* that are, and the rest (reserved, parameter sets, SEI) say nothing of the picture.
|
|
16
|
+
*/
|
|
17
|
+
var HEVC_TYPE_SHIFT = 1;
|
|
18
|
+
var HEVC_TYPE_MASK = 63;
|
|
19
|
+
var HEVC_FIRST_IRAP = 16;
|
|
20
|
+
var HEVC_LAST_IRAP = 23;
|
|
21
|
+
/**
|
|
22
|
+
* Only an IDR slice starts decoding. mediabunny also takes a recovery-point SEI outside
|
|
23
|
+
* Chromium, where decoding from it may show broken frames first; the player never starts there.
|
|
24
|
+
*/
|
|
25
|
+
var AVC = {
|
|
26
|
+
typeOf: (header) => header & AVC_TYPE_MASK,
|
|
27
|
+
verdictOn: (type) => {
|
|
28
|
+
if (type === AVC_IDR_SLICE) return "keyframe";
|
|
29
|
+
return type >= AVC_FIRST_NON_IDR_SLICE && type <= AVC_LAST_SLICE_PARTITION ? "not-a-keyframe" : "read-on";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var HEVC = {
|
|
33
|
+
typeOf: (header) => header >> HEVC_TYPE_SHIFT & HEVC_TYPE_MASK,
|
|
34
|
+
verdictOn: (type) => {
|
|
35
|
+
if (type < HEVC_FIRST_IRAP) return "not-a-keyframe";
|
|
36
|
+
return type <= HEVC_LAST_IRAP ? "keyframe" : "read-on";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var BITS_PER_BYTE = 8;
|
|
40
|
+
/**
|
|
41
|
+
* Reads a sample's NAL units one after another, each after its length, until one tells whether
|
|
42
|
+
* the picture is a keyframe; a sample that never tells, or whose units run past its end, is not
|
|
43
|
+
* one.
|
|
44
|
+
*/
|
|
45
|
+
var NalKeyframeRule = class {
|
|
46
|
+
family;
|
|
47
|
+
lengthSize;
|
|
48
|
+
constructor(family, lengthSize) {
|
|
49
|
+
this.family = family;
|
|
50
|
+
this.lengthSize = lengthSize;
|
|
51
|
+
}
|
|
52
|
+
isKeyframe(sample) {
|
|
53
|
+
let offset = 0;
|
|
54
|
+
while (offset + this.lengthSize < sample.length) {
|
|
55
|
+
const unitStart = offset + this.lengthSize;
|
|
56
|
+
const unitEnd = unitStart + lengthAt(sample, offset, this.lengthSize);
|
|
57
|
+
if (unitEnd > sample.length) return false;
|
|
58
|
+
const verdict = this.family.verdictOn(this.family.typeOf(sample[unitStart] ?? 0));
|
|
59
|
+
if (verdict !== "read-on") return verdict === "keyframe";
|
|
60
|
+
offset = unitEnd;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
function lengthAt(sample, offset, size) {
|
|
66
|
+
let length = 0;
|
|
67
|
+
for (let index = 0; index < size; index += 1) length = length * 2 ** BITS_PER_BYTE + (sample[offset + index] ?? 0);
|
|
68
|
+
return length;
|
|
69
|
+
}
|
|
70
|
+
var FAMILIES_BY_SAMPLE_ENTRY = [[AVC_SAMPLE_ENTRIES, AVC], [HEVC_SAMPLE_ENTRIES, HEVC]];
|
|
71
|
+
/**
|
|
72
|
+
* The rule for a track's codec, by its sample entry. A codec without NAL units, or whose length
|
|
73
|
+
* size is unknown, keeps its sample table's word.
|
|
74
|
+
*/
|
|
75
|
+
function keyframeRuleFor(entry) {
|
|
76
|
+
const { sampleEntryType, nalLengthSize } = entry;
|
|
77
|
+
const [, family] = FAMILIES_BY_SAMPLE_ENTRY.find(([entries]) => entries.includes(sampleEntryType)) ?? [];
|
|
78
|
+
return family && nalLengthSize !== void 0 ? new NalKeyframeRule(family, nalLengthSize) : EVERY_SAMPLE_IS_A_KEYFRAME;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { keyframeRuleFor };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { ByteReader } from "../../../shared/binary/ByteReader.js";
|
|
3
|
+
import { boxHeaderOf } from "../boxes/boxHeader.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/format/mp4/movieBoxes.ts
|
|
5
|
+
/**
|
|
6
|
+
* The boxes `bytes` holds one after another: the children of a container box. Fewer bytes than
|
|
7
|
+
* a header at the end are padding, which some muxers leave; any other bytes that form no box are
|
|
8
|
+
* a movie box the player cannot read.
|
|
9
|
+
*/
|
|
10
|
+
function boxesIn(bytes) {
|
|
11
|
+
const boxes = [];
|
|
12
|
+
let offset = 0;
|
|
13
|
+
while (bytes.byteLength - offset >= 8) {
|
|
14
|
+
const room = bytes.byteLength - offset;
|
|
15
|
+
const headerBytes = bytes.subarray(offset, offset + 16);
|
|
16
|
+
const header = boxHeaderOf(new ByteReader(headerBytes), room);
|
|
17
|
+
if (!header) throw unreadableMovie(`holds no box at byte ${offset} of ${bytes.byteLength}`);
|
|
18
|
+
const body = bytes.subarray(offset + header.headerSize, offset + header.size);
|
|
19
|
+
boxes.push({
|
|
20
|
+
type: header.type,
|
|
21
|
+
body
|
|
22
|
+
});
|
|
23
|
+
offset += header.size;
|
|
24
|
+
}
|
|
25
|
+
return boxes;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The first box of a type the movie box cannot do without.
|
|
29
|
+
*/
|
|
30
|
+
function requiredBox(boxes, type) {
|
|
31
|
+
const box = optionalBox(boxes, type);
|
|
32
|
+
if (!box) throw unreadableMovie(`has no ${type} box where one belongs`);
|
|
33
|
+
return box;
|
|
34
|
+
}
|
|
35
|
+
function optionalBox(boxes, type) {
|
|
36
|
+
return boxes.find((box) => box.type === type);
|
|
37
|
+
}
|
|
38
|
+
function fullBoxOf(box) {
|
|
39
|
+
if (box.body.byteLength < 4) throw unreadableMovie(`has a ${box.type} box too short for its version and flags`);
|
|
40
|
+
return {
|
|
41
|
+
version: box.body[0] ?? 0,
|
|
42
|
+
content: new ByteReader(box.body.subarray(4))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A movie box whose bytes do not describe the samples as ISO/IEC 14496-12 lays them out.
|
|
47
|
+
*/
|
|
48
|
+
function unreadableMovie(what) {
|
|
49
|
+
return new GyroViewError("unsupported-container", `the movie box ${what}`);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { boxesIn, fullBoxOf, optionalBox, requiredBox, unreadableMovie };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/format/mp4/mp4BoxTypes.ts
|
|
2
|
+
/**
|
|
3
|
+
* The MP4 boxes the player reads or refuses, by the names ISO/IEC 14496-12 gives them.
|
|
4
|
+
*/
|
|
5
|
+
var Mp4BoxType = {
|
|
6
|
+
FileType: "ftyp",
|
|
7
|
+
MediaData: "mdat",
|
|
8
|
+
Movie: "moov",
|
|
9
|
+
MovieHeader: "mvhd",
|
|
10
|
+
MovieExtends: "mvex",
|
|
11
|
+
Track: "trak",
|
|
12
|
+
TrackHeader: "tkhd",
|
|
13
|
+
Edit: "edts",
|
|
14
|
+
EditList: "elst",
|
|
15
|
+
Media: "mdia",
|
|
16
|
+
MediaHeader: "mdhd",
|
|
17
|
+
Handler: "hdlr",
|
|
18
|
+
MediaInformation: "minf",
|
|
19
|
+
SampleTable: "stbl",
|
|
20
|
+
SampleDescription: "stsd",
|
|
21
|
+
TimeToSample: "stts",
|
|
22
|
+
CompositionOffset: "ctts",
|
|
23
|
+
SyncSample: "stss",
|
|
24
|
+
SampleToChunk: "stsc",
|
|
25
|
+
SampleSize: "stsz",
|
|
26
|
+
ChunkOffset: "stco",
|
|
27
|
+
ChunkLargeOffset: "co64",
|
|
28
|
+
AvcConfiguration: "avcC",
|
|
29
|
+
HevcConfiguration: "hvcC"
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* A track's handler type (ISO/IEC 14496-12 §8.4.3): what its samples are. Tracks of any other
|
|
33
|
+
* handler (timed metadata, hints) are not played.
|
|
34
|
+
*/
|
|
35
|
+
var HandlerType = {
|
|
36
|
+
Video: "vide",
|
|
37
|
+
Sound: "soun"
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The sample entries of the codecs the cameras record, by family: the parameter sets travel in
|
|
41
|
+
* the sample entry (`avc1`, `hvc1`) or in the stream as well (`avc3`, `hev1`); ISO/IEC 14496-15.
|
|
42
|
+
*/
|
|
43
|
+
var AVC_SAMPLE_ENTRIES = ["avc1", "avc3"];
|
|
44
|
+
var HEVC_SAMPLE_ENTRIES = ["hvc1", "hev1"];
|
|
45
|
+
//#endregion
|
|
46
|
+
export { AVC_SAMPLE_ENTRIES, HEVC_SAMPLE_ENTRIES, HandlerType, Mp4BoxType };
|
|
@@ -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 };
|