@bubo-squared/gyroview 0.2.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +16 -5
  3. package/dist/index.d.ts +1 -0
  4. package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
  5. package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
  6. package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
  7. package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
  8. package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
  9. package/dist/packages/adapters/fetch/src/index.js +3 -1
  10. package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
  11. package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
  12. package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
  13. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
  14. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
  15. package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
  16. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
  17. package/dist/packages/adapters/mediabunny/src/index.js +3 -2
  18. package/dist/packages/core/src/application/download/BlockStore.js +97 -0
  19. package/dist/packages/core/src/application/download/DownloadedAudioSamples.js +50 -0
  20. package/dist/packages/core/src/application/download/DownloadedVideoTrack.js +122 -0
  21. package/dist/packages/core/src/application/download/FileDownload.js +183 -0
  22. package/dist/packages/core/src/application/download/RecordingBuffer.js +22 -0
  23. package/dist/packages/core/src/application/download/SampleCursor.js +88 -0
  24. package/dist/packages/core/src/application/download/SourceByteStream.js +46 -0
  25. package/dist/packages/core/src/application/download/Transfers.js +75 -0
  26. package/dist/packages/core/src/application/download/startFileDownload.js +37 -0
  27. package/dist/packages/core/src/application/playback/Buffering.js +53 -0
  28. package/dist/packages/core/src/application/playback/DecodePipeline.js +11 -7
  29. package/dist/packages/core/src/application/playback/DecodeRun.js +15 -0
  30. package/dist/packages/core/src/application/playback/PlaybackSession.js +18 -27
  31. package/dist/packages/core/src/application/playback/keyframeTimeAt.js +1 -1
  32. package/dist/packages/core/src/application/playback/probeDecoding.js +15 -1
  33. package/dist/packages/core/src/application/recording/readSampleTable.js +27 -0
  34. package/dist/packages/core/src/application/recording/timeRecording.js +3 -3
  35. package/dist/packages/core/src/domain/container/KeyframeRule.js +7 -0
  36. package/dist/packages/core/src/domain/container/SampleTable.js +29 -0
  37. package/dist/packages/core/src/domain/container/TrackSampleTable.js +96 -0
  38. package/dist/packages/core/src/domain/container/presentationOrder.js +10 -0
  39. package/dist/packages/core/src/domain/download/DownloadPolicy.js +41 -0
  40. package/dist/packages/core/src/domain/download/isReadyToResume.js +27 -0
  41. package/dist/packages/core/src/domain/download/planDownloads.js +175 -0
  42. package/dist/packages/core/src/domain/download/trackNeeds.js +32 -0
  43. package/dist/packages/core/src/domain/format/boxes/boxHeader.js +44 -0
  44. package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +7 -34
  45. package/dist/packages/core/src/domain/format/mp4/keyframeRules.js +81 -0
  46. package/dist/packages/core/src/domain/format/mp4/movieBoxes.js +52 -0
  47. package/dist/packages/core/src/domain/format/mp4/mp4BoxTypes.js +46 -0
  48. package/dist/packages/core/src/domain/format/mp4/mp4Layouts.js +183 -0
  49. package/dist/packages/core/src/domain/format/mp4/parseMovie.js +67 -0
  50. package/dist/packages/core/src/domain/format/mp4/readTable.js +31 -0
  51. package/dist/packages/core/src/domain/format/mp4/sampleLocations.js +69 -0
  52. package/dist/packages/core/src/domain/format/mp4/sampleTiming.js +109 -0
  53. package/dist/packages/core/src/domain/format/mp4/trackHeaders.js +75 -0
  54. package/dist/packages/core/src/index.js +9 -1
  55. package/dist/packages/core/src/shared/async/iteration.js +24 -0
  56. package/dist/packages/core/src/shared/binary/ByteRange.js +12 -0
  57. package/dist/packages/core/src/shared/binary/ByteRangeSet.js +85 -0
  58. package/dist/packages/core/src/shared/binary/ByteReader.js +21 -3
  59. package/dist/packages/core/src/shared/binary/concatenated.js +15 -0
  60. package/dist/packages/core/src/shared/errors/GyroViewError.js +1 -0
  61. package/dist/packages/core/src/shared/math/countAtOrBelow.js +17 -0
  62. package/dist/packages/player/src/composition/browserPorts.js +23 -4
  63. package/dist/packages/player/src/composition/buildPipeline.js +15 -3
  64. package/dist/packages/player/src/composition/openInputs.js +51 -107
  65. package/dist/packages/player/src/composition/readInputs.js +111 -0
  66. package/dist/packages/player/src/controls/messages.js +1 -0
  67. package/dist/packages/player/src/inspectRecording.js +1 -1
  68. package/dist/standalone.js +58 -58
  69. package/package.json +1 -1
  70. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
  71. package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
  72. package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
@@ -7,9 +7,11 @@ import "../../../core/src/index.js";
7
7
  */
8
8
  var FIRST_BYTE_RANGE = "bytes=0-0";
9
9
  /**
10
- * The response headers a cross-origin page must be let read, as the fix a CORS failure names.
10
+ * The response headers a cross-origin page must be let read, as the fix a CORS failure names;
11
+ * `ETag` is optional, and tells a recording replaced while it plays more surely than
12
+ * `Last-Modified`, which a page may always read.
11
13
  */
12
- var EXPOSED_HEADERS_ADVICE = "Access-Control-Expose-Headers: Content-Range, Content-Length, Accept-Ranges";
14
+ var EXPOSED_HEADERS_ADVICE = "Access-Control-Expose-Headers: Content-Range, Content-Length, Accept-Ranges, ETag";
13
15
  /**
14
16
  * What a server must add for a page to read it across origins. A request that carries the
15
17
  * visitor's cookies is held to more: the Fetch standard refuses a wildcard origin for it.
@@ -20,7 +22,7 @@ var CREDENTIALED_CORS_ADVICE = `Access-Control-Allow-Origin naming this page's o
20
22
  * Browsers keep byte ranges of one URL as a sparse cache entry and revalidate them with
21
23
  * conditional requests. A 304 to such a request makes the browser answer from that entry, and
22
24
  * Chrome has been seen to hand back an empty body for a range it believed it held (ADR 0013).
23
- * The demuxer caches what it needs itself, so nothing is lost by leaving the cache out.
25
+ * The download holds what it needs itself, so nothing is lost by leaving the cache out.
24
26
  */
25
27
  var CACHE_MODE = "no-store";
26
28
  /**
@@ -1,3 +1,5 @@
1
+ import { HttpByteStream } from "./HttpByteStream.js";
1
2
  import { HttpRangeSource } from "./HttpRangeSource.js";
3
+ import { HttpResource } from "./HttpResource.js";
2
4
  import { HttpResourceLocator } from "./HttpResourceLocator.js";
3
- export { HttpRangeSource, HttpResourceLocator };
5
+ export { HttpByteStream, HttpRangeSource, HttpResource, HttpResourceLocator };
@@ -0,0 +1,24 @@
1
+ import { GyroViewError, hasErrorCode } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import "../../../core/src/index.js";
3
+ //#region ../../packages/adapters/fetch/src/passingFailures.ts
4
+ /**
5
+ * The failures on the way, which asking again may cure; any other fails the read at once. They
6
+ * are `source-unreadable` errors like any other, so a caller that gets one after the retries
7
+ * cannot tell, and need not: only the retry loop asks.
8
+ */
9
+ var passingFailures = /* @__PURE__ */ new WeakSet();
10
+ function passing(failure) {
11
+ passingFailures.add(failure);
12
+ return failure;
13
+ }
14
+ function isPassing(error) {
15
+ return error instanceof GyroViewError && passingFailures.has(error);
16
+ }
17
+ /**
18
+ * A request that did not get through for want of a network is worth another try.
19
+ */
20
+ function passingIfUnreachable(error) {
21
+ return hasErrorCode(error, "source-unreadable") && error instanceof Error ? passing(new GyroViewError("source-unreadable", error.message, { cause: error.cause })) : error;
22
+ }
23
+ //#endregion
24
+ export { isPassing, passing, passingIfUnreachable };
@@ -0,0 +1,17 @@
1
+ import { GyroViewError } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import "../../../core/src/index.js";
3
+ import { passing } from "./passingFailures.js";
4
+ //#region ../../packages/adapters/fetch/src/rangeFailures.ts
5
+ /**
6
+ * A body a dropped connection broke off after the headers: the most common way a network fails
7
+ * during long playback, so it is `source-unreadable` as a refused request is, not a bug or a bad
8
+ * file, and worth another try.
9
+ */
10
+ function brokenOff(url, range, cause) {
11
+ return passing(new GyroViewError("source-unreadable", `${url} broke off the ${range.length}-byte range at ${range.offset}`, { cause }));
12
+ }
13
+ function truncated(url, received, range) {
14
+ return new GyroViewError("source-truncated", `${url} returned ${received} bytes for a ${range.length}-byte range at ${range.offset}`);
15
+ }
16
+ //#endregion
17
+ export { brokenOff, truncated };
@@ -0,0 +1,37 @@
1
+ import { contentRangeOf } from "./contentRange.js";
2
+ //#region ../../packages/adapters/fetch/src/recordingVersion.ts
3
+ var HTTP_OK = 200;
4
+ var WEAK_ETAG_PREFIX = /^W\//u;
5
+ function versionOf(response) {
6
+ const etag = response.headers.get("etag");
7
+ const lastModified = response.headers.get("last-modified");
8
+ const size = sizeOf(response);
9
+ return {
10
+ ...etag !== null && { etag },
11
+ ...lastModified !== null && { lastModified },
12
+ ...size !== void 0 && { size }
13
+ };
14
+ }
15
+ /**
16
+ * Whether two answers are of one version of the recording, by the ETag where both tell one,
17
+ * else by the Last-Modified and the size; what either does not tell is taken for the same, so
18
+ * a server that tells nothing is never taken for one that changed its file.
19
+ */
20
+ function isSameVersion(known, seen) {
21
+ return known.etag !== void 0 && seen.etag !== void 0 ? opaqueTagOf(known.etag) === opaqueTagOf(seen.etag) : isSameWhereTold(known.lastModified, seen.lastModified) && isSameWhereTold(known.size, seen.size);
22
+ }
23
+ /**
24
+ * The tag without its weakness mark: a CDN may weaken a strong ETag it passes on.
25
+ */
26
+ function opaqueTagOf(etag) {
27
+ return etag.replace(WEAK_ETAG_PREFIX, "");
28
+ }
29
+ function isSameWhereTold(known, seen) {
30
+ return known === void 0 || seen === void 0 || known === seen;
31
+ }
32
+ function sizeOf(response) {
33
+ const size = response.status === HTTP_OK ? Number(response.headers.get("content-length") ?? NaN) : contentRangeOf(response)?.total;
34
+ return size !== void 0 && Number.isSafeInteger(size) && size > 0 ? size : void 0;
35
+ }
36
+ //#endregion
37
+ export { isSameVersion, versionOf };
@@ -0,0 +1,129 @@
1
+ import { GyroViewError } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import "../../../core/src/index.js";
3
+ import { SegmentChannel } from "./SegmentChannel.js";
4
+ import { EncodedAudioPacketSource, EncodedPacket, Mp4OutputFormat, NullTarget, Output } from "mediabunny";
5
+ //#region ../../packages/adapters/mediabunny/src/MediabunnyAudioPackager.ts
6
+ /**
7
+ * Seconds of audio per media segment: short enough that a seek has sound quickly, long enough
8
+ * that appends stay cheap.
9
+ */
10
+ var FRAGMENT_DURATION_SECONDS = 1;
11
+ /**
12
+ * Segments (each fragment is two: `moof` and `mdat`) that may wait for the consumer before
13
+ * re-packaging pauses.
14
+ */
15
+ var SEGMENTS_AHEAD = 4;
16
+ /**
17
+ * The codec strings of the audio the cameras record, AAC (`mp4a.40.2` for AAC-LC), and mediabunny's
18
+ * name for it. Other codecs are not re-packaged: the picture then plays on a silent clock.
19
+ */
20
+ var AAC_CODEC_STRING_PREFIX = "mp4a.40.";
21
+ var AAC = "aac";
22
+ /**
23
+ * AudioPackager over mediabunny: the samples re-packaged, unchanged, into fragmented MP4.
24
+ * Segments are taken from the muxer's box callbacks rather than from its byte stream: that
25
+ * yields whole `ftyp`/`moov`/`moof`/`mdat` boxes and leaves out the `mfra` index the muxer
26
+ * appends at the end, which Media Source Extensions do not accept. Fragmented output keeps the
27
+ * track's own timestamps, so segments started mid-track land at their true time.
28
+ */
29
+ var MediabunnyAudioPackager = class {
30
+ segmentsOf(samples, configuration) {
31
+ const codec = muxedCodecOf(configuration);
32
+ return {
33
+ mimeType: `audio/mp4; codecs="${configuration.codec}"`,
34
+ duration: samples.duration,
35
+ segmentsFrom: (from) => ({ [Symbol.asyncIterator]: () => remux({
36
+ samples,
37
+ configuration,
38
+ codec
39
+ }, from) })
40
+ };
41
+ }
42
+ };
43
+ function muxedCodecOf(configuration) {
44
+ if (configuration.codec.startsWith(AAC_CODEC_STRING_PREFIX)) return AAC;
45
+ throw new GyroViewError("codec-unsupported", `audio in ${configuration.codec} cannot be re-packaged for playback`);
46
+ }
47
+ /**
48
+ * Starts re-packaging from `from` into a channel and hands out its consumer's side. When the
49
+ * consumer leaves, the samples are let go of at once, even while one is awaited, and the muxer
50
+ * once the producer has noticed.
51
+ */
52
+ function remux(parts, from) {
53
+ const channel = new SegmentChannel(SEGMENTS_AHEAD);
54
+ const output = new Output({
55
+ format: fragmentedMp4Into(channel),
56
+ target: new NullTarget()
57
+ });
58
+ const source = new EncodedAudioPacketSource(parts.codec);
59
+ output.addAudioTrack(source);
60
+ const samples = parts.samples.samplesFrom(from)[Symbol.asyncIterator]();
61
+ const production = produce({
62
+ samples,
63
+ configuration: parts.configuration,
64
+ output,
65
+ source,
66
+ channel
67
+ });
68
+ return channel.segments(() => {
69
+ samples.return?.();
70
+ releaseAfter(production, output);
71
+ });
72
+ }
73
+ /**
74
+ * Frees the muxer once the producer has noticed the closed channel, without making a consumer
75
+ * that left early (a seek) wait for a sample still in flight.
76
+ */
77
+ async function releaseAfter(production, output) {
78
+ await production;
79
+ if (output.state === "finalized") return;
80
+ try {
81
+ await output.cancel();
82
+ } catch {}
83
+ }
84
+ /**
85
+ * Feeds samples to the muxer at the consumer's pace; every failure ends up in the channel.
86
+ */
87
+ async function produce(run) {
88
+ try {
89
+ await run.output.start();
90
+ let meta = { decoderConfig: decoderConfigOf(run.configuration) };
91
+ for (let next = await run.samples.next(); next.done !== true; next = await run.samples.next()) {
92
+ await run.channel.waitForRoom();
93
+ if (run.channel.isClosed) return;
94
+ await run.source.add(packetOf(next.value), meta);
95
+ meta = void 0;
96
+ }
97
+ if (run.channel.isClosed) return;
98
+ await run.output.finalize();
99
+ run.channel.close();
100
+ } catch (error) {
101
+ run.channel.fail(error);
102
+ }
103
+ }
104
+ function packetOf(sample) {
105
+ return new EncodedPacket(sample.data, "key", sample.timestamp, sample.duration);
106
+ }
107
+ function decoderConfigOf(configuration) {
108
+ return {
109
+ codec: configuration.codec,
110
+ sampleRate: configuration.sampleRate,
111
+ numberOfChannels: configuration.numberOfChannels,
112
+ ...configuration.description && { description: configuration.description }
113
+ };
114
+ }
115
+ function fragmentedMp4Into(channel) {
116
+ const forward = (data) => {
117
+ channel.push(new Uint8Array(data));
118
+ };
119
+ return new Mp4OutputFormat({
120
+ fastStart: "fragmented",
121
+ minimumFragmentDuration: FRAGMENT_DURATION_SECONDS,
122
+ onFtyp: forward,
123
+ onMoov: forward,
124
+ onMoof: forward,
125
+ onMdat: forward
126
+ });
127
+ }
128
+ //#endregion
129
+ export { MediabunnyAudioPackager };
@@ -0,0 +1,80 @@
1
+ import { GyroViewError, isAbortError } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import "../../../core/src/index.js";
3
+ import { audioConfigurationOf, videoConfigurationOf } from "./decoderConfigurations.js";
4
+ import { ALL_FORMATS, BufferSource, Input } from "mediabunny";
5
+ //#region ../../packages/adapters/mediabunny/src/MediabunnyCodecReader.ts
6
+ /**
7
+ * CodecReader over mediabunny: opens the file type and movie boxes as a file of their own, in
8
+ * memory, and tells each track's decoder configuration. mediabunny reads a sample only for a
9
+ * video codec without its configuration box; the samples are not there, so that track is
10
+ * refused.
11
+ */
12
+ var MediabunnyCodecReader = class {
13
+ async read(movieBytes) {
14
+ const input = new Input({
15
+ formats: ALL_FORMATS,
16
+ source: new BufferSource(movieBytes)
17
+ });
18
+ try {
19
+ const [videoTracks, audioTracks] = await Promise.all([input.getVideoTracks(), input.getAudioTracks()]);
20
+ return {
21
+ video: await Promise.all(videoTracks.map((track, index) => videoCodecOf(track, index))),
22
+ audio: (await Promise.all(audioTracks.map((track) => audioCodecOf(track)))).flat()
23
+ };
24
+ } catch (error) {
25
+ throw failureOf(error);
26
+ } finally {
27
+ input.dispose();
28
+ }
29
+ }
30
+ };
31
+ async function videoCodecOf(track, trackIndex) {
32
+ const [config, codedWidth, codedHeight] = await Promise.all([
33
+ configurationOrNull(track),
34
+ track.getCodedWidth(),
35
+ track.getCodedHeight()
36
+ ]);
37
+ if (config === null) throw new GyroViewError("codec-unsupported", `video track ${trackIndex} has no decoder configuration in its sample entry`);
38
+ const description = {
39
+ trackIndex,
40
+ codedWidth,
41
+ codedHeight,
42
+ codec: config.codec
43
+ };
44
+ return {
45
+ trackId: track.id,
46
+ description,
47
+ configuration: videoConfigurationOf(config, description)
48
+ };
49
+ }
50
+ /**
51
+ * The configuration of a codec that carries it in the movie box; null for one that would need
52
+ * a sample, which the movie bytes do not hold.
53
+ */
54
+ async function configurationOrNull(track) {
55
+ try {
56
+ return await track.getDecoderConfig();
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * An audio track mediabunny cannot configure is left out: the picture then plays without sound
63
+ * rather than not at all.
64
+ */
65
+ async function audioCodecOf(track) {
66
+ const config = await track.getDecoderConfig();
67
+ return config === null ? [] : [{
68
+ trackId: track.id,
69
+ configuration: audioConfigurationOf(config)
70
+ }];
71
+ }
72
+ /**
73
+ * A caller's abort and the core's own failures pass as they are; anything else mediabunny
74
+ * throws means the bytes are no movie it can read.
75
+ */
76
+ function failureOf(error) {
77
+ return isAbortError(error) || error instanceof GyroViewError ? error : new GyroViewError("unsupported-container", "the movie box is not a readable movie", { cause: error });
78
+ }
79
+ //#endregion
80
+ export { MediabunnyCodecReader };
@@ -1,4 +1,5 @@
1
1
  import { GyroViewError, ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
2
+ import { ITERATION_END } from "../../../core/src/shared/async/iteration.js";
2
3
  import "../../../core/src/index.js";
3
4
  //#region ../../packages/adapters/mediabunny/src/SegmentChannel.ts
4
5
  /**
@@ -43,12 +44,20 @@ var SegmentChannel = class {
43
44
  this.failure ??= error instanceof Error ? error : new GyroViewError("decode", "audio re-packaging failed", { cause: error });
44
45
  this.close();
45
46
  }
46
- async *segments() {
47
- let next = await this.take();
48
- while (next) {
49
- yield next;
50
- next = await this.take();
51
- }
47
+ /**
48
+ * The consumer's side: the segments in order until the channel is closed and drained, then
49
+ * the producer's failure if it had one. Returning closes the channel at once, even while a
50
+ * segment is awaited, which then comes as the end. `onEnd` hears once of the end, however it
51
+ * came: the producer's resources may go.
52
+ */
53
+ segments(onEnd = doNothing) {
54
+ return new SegmentReading({
55
+ take: () => this.take(),
56
+ close: () => {
57
+ this.close();
58
+ },
59
+ onEnd
60
+ });
52
61
  }
53
62
  /**
54
63
  * The next segment; undefined once closed and drained; the producer's failure, if it had one.
@@ -80,5 +89,48 @@ var SegmentChannel = class {
80
89
  for (const resolve of waiters) resolve();
81
90
  }
82
91
  };
92
+ /**
93
+ * Iterates a channel's segments. A class, not a generator: a generator's return waits behind the
94
+ * segment it awaits, and the producer would re-package another fragment for nobody.
95
+ */
96
+ var SegmentReading = class {
97
+ parts;
98
+ hasEnded = false;
99
+ constructor(parts) {
100
+ this.parts = parts;
101
+ }
102
+ async next() {
103
+ if (this.hasEnded) return ITERATION_END;
104
+ try {
105
+ const segment = await this.parts.take();
106
+ return segment === void 0 || this.wasEnded() ? this.end() : {
107
+ done: false,
108
+ value: segment
109
+ };
110
+ } catch (error) {
111
+ this.end();
112
+ throw error;
113
+ }
114
+ }
115
+ return() {
116
+ this.parts.close();
117
+ return Promise.resolve(this.end());
118
+ }
119
+ [Symbol.asyncIterator]() {
120
+ return this;
121
+ }
122
+ /**
123
+ * Asked afresh after the wait: the consumer may have returned meanwhile.
124
+ */
125
+ wasEnded() {
126
+ return this.hasEnded;
127
+ }
128
+ end() {
129
+ if (!this.hasEnded) this.parts.onEnd();
130
+ this.hasEnded = true;
131
+ return ITERATION_END;
132
+ }
133
+ };
134
+ function doNothing() {}
83
135
  //#endregion
84
136
  export { SegmentChannel };
@@ -0,0 +1,28 @@
1
+ import { copyOfBytes } from "./bufferSources.js";
2
+ //#region ../../packages/adapters/mediabunny/src/decoderConfigurations.ts
3
+ /**
4
+ * A WebCodecs video decoder configuration in the core's terms; the coded size comes from the
5
+ * track where the configuration leaves it out.
6
+ */
7
+ function videoConfigurationOf(config, description) {
8
+ return {
9
+ codec: config.codec,
10
+ codedWidth: config.codedWidth ?? description.codedWidth,
11
+ codedHeight: config.codedHeight ?? description.codedHeight,
12
+ description: config.description === void 0 ? void 0 : copyOfBytes(config.description),
13
+ isFullRange: config.colorSpace?.fullRange ?? void 0
14
+ };
15
+ }
16
+ /**
17
+ * A WebCodecs audio decoder configuration in the core's terms.
18
+ */
19
+ function audioConfigurationOf(config) {
20
+ return {
21
+ codec: config.codec,
22
+ sampleRate: config.sampleRate,
23
+ numberOfChannels: config.numberOfChannels,
24
+ description: config.description === void 0 ? void 0 : copyOfBytes(config.description)
25
+ };
26
+ }
27
+ //#endregion
28
+ export { audioConfigurationOf, videoConfigurationOf };
@@ -1,2 +1,3 @@
1
- import { MediabunnyDemuxer } from "./MediabunnyDemuxer.js";
2
- export { MediabunnyDemuxer };
1
+ import { MediabunnyCodecReader } from "./MediabunnyCodecReader.js";
2
+ import { MediabunnyAudioPackager } from "./MediabunnyAudioPackager.js";
3
+ export { MediabunnyAudioPackager, MediabunnyCodecReader };
@@ -0,0 +1,97 @@
1
+ import { ensureInvariant } from "../../shared/errors/GyroViewError.js";
2
+ import { ByteRange } from "../../shared/binary/ByteRange.js";
3
+ import { ByteRangeSet } from "../../shared/binary/ByteRangeSet.js";
4
+ //#region ../../packages/core/src/application/download/BlockStore.ts
5
+ var FillingBlock = class {
6
+ range;
7
+ bytes;
8
+ filled = 0;
9
+ /**
10
+ * No more of its bytes will come: all have, or its range was given up.
11
+ */
12
+ isComplete = false;
13
+ constructor(range, bytes) {
14
+ this.range = range;
15
+ this.bytes = bytes;
16
+ }
17
+ get filledRange() {
18
+ return ByteRange.of(this.range.offset, this.filled);
19
+ }
20
+ };
21
+ /**
22
+ * The bytes a download holds, a block for each range it asked for. A range within one block is
23
+ * handed out as a view of it; one across blocks is copied out of them.
24
+ */
25
+ var BlockStore = class {
26
+ blocks = [];
27
+ get held() {
28
+ return ByteRangeSet.of(this.blocks.map((block) => block.filledRange));
29
+ }
30
+ /**
31
+ * The memory the blocks take, whether their bytes have come or not.
32
+ */
33
+ get allocatedBytes() {
34
+ return this.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
35
+ }
36
+ allocate(range) {
37
+ const block = new FillingBlock(range, new Uint8Array(range.length));
38
+ this.blocks.push(block);
39
+ return block;
40
+ }
41
+ write(block, chunk) {
42
+ const filling = this.fillingOf(block);
43
+ filling.bytes.set(chunk, filling.filled);
44
+ filling.filled += chunk.byteLength;
45
+ }
46
+ /**
47
+ * Every byte of the block has come.
48
+ */
49
+ complete(block) {
50
+ this.fillingOf(block).isComplete = true;
51
+ }
52
+ /**
53
+ * Keeps of a block only what has come, as when its range is given up; a block that got
54
+ * nothing goes.
55
+ */
56
+ trim(block) {
57
+ const filling = this.fillingOf(block);
58
+ filling.isComplete = true;
59
+ filling.bytes = filling.bytes.slice(0, filling.filled);
60
+ this.blocks = this.blocks.filter((held) => held.bytes.byteLength > 0);
61
+ }
62
+ /**
63
+ * The bytes of `range`, if every one of them has come.
64
+ */
65
+ bytesOf(range) {
66
+ const within = this.blocks.find((block) => block.filledRange.contains(range));
67
+ if (within) return viewOf(within, range);
68
+ return this.held.covers(range) ? this.copyOf(range) : void 0;
69
+ }
70
+ /**
71
+ * Lets go of the complete blocks whose every byte `released` holds; a block still filling is
72
+ * its transfer's until it ends.
73
+ */
74
+ release(released) {
75
+ this.blocks = this.blocks.filter((block) => !block.isComplete || !released.covers(block.filledRange));
76
+ }
77
+ fillingOf(block) {
78
+ const filling = this.blocks.find((held) => held === block);
79
+ ensureInvariant(filling !== void 0, "the block is not one this store holds");
80
+ return filling;
81
+ }
82
+ copyOf(range) {
83
+ const copy = new Uint8Array(range.length);
84
+ for (const block of this.blocks) {
85
+ const start = Math.max(block.range.offset, range.offset);
86
+ const end = Math.min(block.filledRange.end, range.end);
87
+ if (start < end) copy.set(block.bytes.subarray(start - block.range.offset, end - block.range.offset), start - range.offset);
88
+ }
89
+ return copy;
90
+ }
91
+ };
92
+ function viewOf(block, range) {
93
+ const start = range.offset - block.range.offset;
94
+ return block.bytes.subarray(start, start + range.length);
95
+ }
96
+ //#endregion
97
+ export { BlockStore };
@@ -0,0 +1,50 @@
1
+ import { Ending, ITERATION_END } from "../../shared/async/iteration.js";
2
+ //#region ../../packages/core/src/application/download/DownloadedAudioSamples.ts
3
+ /**
4
+ * A sound track's samples read through the file's download, within the picture's reach.
5
+ */
6
+ var DownloadedAudioSamples = class {
7
+ parts;
8
+ constructor(parts) {
9
+ this.parts = parts;
10
+ }
11
+ get duration() {
12
+ return this.parts.track.end;
13
+ }
14
+ samplesFrom(time) {
15
+ const { download, track } = this.parts;
16
+ return { [Symbol.asyncIterator]: () => new SampleReading(track, download.openCursor(track, track.sampleShownAt(time) ?? 0, time)) };
17
+ }
18
+ };
19
+ /**
20
+ * One iteration over the samples; each is copied out of what was downloaded, so the samples a
21
+ * media pipeline keeps do not keep the download's blocks alive.
22
+ */
23
+ var SampleReading = class {
24
+ track;
25
+ cursor;
26
+ ending = new Ending();
27
+ constructor(track, cursor) {
28
+ this.track = track;
29
+ this.cursor = cursor;
30
+ }
31
+ async next() {
32
+ const read = await this.cursor.nextSample();
33
+ if (!read || this.ending.hasEnded()) return ITERATION_END;
34
+ return {
35
+ done: false,
36
+ value: {
37
+ timestamp: this.track.timestampOf(read.sample),
38
+ duration: this.track.durationOf(read.sample),
39
+ data: Uint8Array.from(read.bytes)
40
+ }
41
+ };
42
+ }
43
+ return() {
44
+ this.ending.end();
45
+ this.cursor.close();
46
+ return Promise.resolve(ITERATION_END);
47
+ }
48
+ };
49
+ //#endregion
50
+ export { DownloadedAudioSamples };