@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +35 -15
  3. package/dist/index.d.ts +21 -2
  4. package/dist/index.js +2 -2
  5. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  6. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  7. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  8. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  9. package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
  10. package/dist/packages/adapters/fetch/src/index.js +3 -1
  11. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  12. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  13. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  15. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  16. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  17. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  18. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  19. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +12 -3
  20. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  21. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  22. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  23. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  24. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  25. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  26. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  27. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  28. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  29. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  30. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  31. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  32. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  33. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  34. package/dist/packages/core/src/application/playback/probeDecoding.js +17 -3
  35. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  36. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  37. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  38. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  39. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  40. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  41. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  42. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  43. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  44. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  45. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  46. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  47. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  48. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  49. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  50. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  51. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  52. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  53. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  54. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  55. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  56. package/dist/packages/core/src/index.js +10 -2
  57. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  58. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  59. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  60. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  61. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  62. package/dist/packages/core/src/shared/errors/GyroViewError.js +56 -3
  63. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  64. package/dist/packages/player/src/composition/browserPorts.js +23 -4
  65. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  66. package/dist/packages/player/src/composition/openInputs.js +51 -107
  67. package/dist/packages/player/src/composition/readInputs.js +111 -0
  68. package/dist/packages/player/src/controls/messages.js +3 -1
  69. package/dist/packages/player/src/inspectRecording.js +1 -1
  70. package/dist/standalone.js +58 -58
  71. package/package.json +1 -1
  72. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  73. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  74. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -0,0 +1,41 @@
1
+ import { seconds } from "../../shared/units/time.js";
2
+ //#region ../../packages/core/src/domain/download/DownloadPolicy.ts
3
+ var MEBIBYTE = 1048576;
4
+ /**
5
+ * What the policy reads ahead and keeps behind, as time and at most as bytes: the byte limits
6
+ * bind first on the X5's 210 Mbit/s, the times on slower recordings. A split recording shares
7
+ * the byte limits among its files.
8
+ */
9
+ var AHEAD_SECONDS = 10;
10
+ var MOST_AHEAD_MEBIBYTES = 128;
11
+ var KEEP_BEHIND_SECONDS = 2;
12
+ var MOST_KEPT_BEHIND_MEBIBYTES = 32;
13
+ var REQUEST_MEBIBYTES = 8;
14
+ var REQUESTS_IN_FLIGHT = 2;
15
+ var BRIDGED_GAP_MEBIBYTES = 1;
16
+ var REFILL_FRACTION = .25;
17
+ /**
18
+ * Long enough that a link slower than the recording plays in stretches rather than frame by
19
+ * frame, short enough that the wait does not feel like a stop.
20
+ */
21
+ var RESUME_SECONDS = 4;
22
+ /**
23
+ * The policy for one file of a recording of `fileCount` files, by its bit rate.
24
+ */
25
+ function downloadPolicyFor(file, fileCount) {
26
+ const bytesPerSecond = file.size / file.duration;
27
+ const aheadBytes = Math.min(Math.round(AHEAD_SECONDS * bytesPerSecond), MOST_AHEAD_MEBIBYTES * MEBIBYTE / fileCount);
28
+ return {
29
+ aheadSeconds: seconds(AHEAD_SECONDS),
30
+ aheadBytes,
31
+ keepBehindBytes: Math.min(Math.round(KEEP_BEHIND_SECONDS * bytesPerSecond), MOST_KEPT_BEHIND_MEBIBYTES * MEBIBYTE / fileCount),
32
+ keepBehindSeconds: seconds(KEEP_BEHIND_SECONDS),
33
+ requestSize: REQUEST_MEBIBYTES * MEBIBYTE,
34
+ requestsInFlight: REQUESTS_IN_FLIGHT,
35
+ bridgedGap: BRIDGED_GAP_MEBIBYTES * MEBIBYTE,
36
+ refillBytes: Math.round(aheadBytes * REFILL_FRACTION),
37
+ resumeSeconds: seconds(RESUME_SECONDS)
38
+ };
39
+ }
40
+ //#endregion
41
+ export { downloadPolicyFor };
@@ -0,0 +1,27 @@
1
+ import { seconds } from "../../shared/units/time.js";
2
+ import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
3
+ import { needsOf, withinBytes } from "./trackNeeds.js";
4
+ //#region ../../packages/core/src/domain/download/isReadyToResume.ts
5
+ /**
6
+ * What playback that starved at `time` waits for before it plays again (ADR 0011, ADR 0029):
7
+ * every picture reader's frames up to the policy's resume seconds past it, or as many of them as
8
+ * the budget holds less a request (a refill's worth, where that is less), so the download that
9
+ * keeps it held still tops up in large ranges. Sound needs nothing of its own.
10
+ */
11
+ function resumeThresholdOf(need) {
12
+ const { policy } = need;
13
+ const until = seconds(need.time + policy.resumeSeconds);
14
+ const pictures = need.cursors.filter((cursor) => cursor.track.kind === "video");
15
+ const needs = ByteRangeSet.of(pictures.flatMap((cursor) => needsOf(cursor, until)));
16
+ const margin = Math.min(policy.requestSize, policy.refillBytes);
17
+ return withinBytes(needs, policy.aheadBytes - margin);
18
+ }
19
+ /**
20
+ * Whether the threshold is held: the download keeps it held once playing, so a download that
21
+ * reads ahead is ready as soon as its bytes have come.
22
+ */
23
+ function isReadyToResume(state) {
24
+ return resumeThresholdOf(state).subtract(state.held).isEmpty;
25
+ }
26
+ //#endregion
27
+ export { isReadyToResume, resumeThresholdOf };
@@ -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 { isPlausibleBoxType } from "./boxConstants.js";
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 headerLength = Math.min(16, fileSize - offset);
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 type = header.asciiAt(4, 4);
26
- if (!isPlausibleBoxType(type)) return void 0;
27
- const resolved = resolveSize(header, offset, fileSize);
28
- if (!resolved) return void 0;
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 };