@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.
- package/CHANGELOG.md +26 -0
- package/README.md +16 -5
- package/dist/index.d.ts +1 -0
- package/dist/packages/adapters/fetch/src/HttpByteStream.js +174 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +14 -170
- package/dist/packages/adapters/fetch/src/HttpResource.js +192 -0
- package/dist/packages/adapters/fetch/src/contentRange.js +15 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +5 -3
- package/dist/packages/adapters/fetch/src/index.js +3 -1
- package/dist/packages/adapters/fetch/src/passingFailures.js +24 -0
- package/dist/packages/adapters/fetch/src/rangeFailures.js +17 -0
- package/dist/packages/adapters/fetch/src/recordingVersion.js +37 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +129 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +80 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +58 -6
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +28 -0
- package/dist/packages/adapters/mediabunny/src/index.js +3 -2
- package/dist/packages/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/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 +23 -4
- package/dist/packages/player/src/composition/buildPipeline.js +15 -3
- package/dist/packages/player/src/composition/openInputs.js +51 -107
- package/dist/packages/player/src/composition/readInputs.js +111 -0
- package/dist/packages/player/src/controls/messages.js +1 -0
- package/dist/packages/player/src/inspectRecording.js +1 -1
- package/dist/standalone.js +58 -58
- package/package.json +1 -1
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +0 -133
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +0 -63
- package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +0 -84
|
@@ -1,130 +1,70 @@
|
|
|
1
|
-
import { GyroViewError, ensureInvariant
|
|
1
|
+
import { GyroViewError, ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
2
|
import { seconds } from "../../../core/src/shared/units/time.js";
|
|
3
|
-
import { readRecording } from "../../../core/src/application/recording/readRecording.js";
|
|
4
3
|
import { timeRecording } from "../../../core/src/application/recording/timeRecording.js";
|
|
5
4
|
import { detectLensLayout } from "../../../core/src/domain/format/layout/detectLensLayout.js";
|
|
6
5
|
import { lensFrameOrder } from "../../../core/src/domain/stitching/StitchingSetup.js";
|
|
6
|
+
import { startFileDownload } from "../../../core/src/application/download/startFileDownload.js";
|
|
7
|
+
import { downloadPolicyFor } from "../../../core/src/domain/download/DownloadPolicy.js";
|
|
8
|
+
import { RecordingBuffer } from "../../../core/src/application/download/RecordingBuffer.js";
|
|
7
9
|
import "../../../core/src/index.js";
|
|
8
|
-
import { inputName } from "../PlayerSource.js";
|
|
9
10
|
import { Disposables } from "./Disposables.js";
|
|
10
11
|
import { ensureDecodable } from "./ensureDecodable.js";
|
|
12
|
+
import { describedInput, readInputs } from "./readInputs.js";
|
|
11
13
|
//#region ../../packages/player/src/composition/openInputs.ts
|
|
12
14
|
/**
|
|
13
|
-
* Opens the given inputs as one recording
|
|
14
|
-
*
|
|
15
|
+
* Opens the given inputs as one recording (ADR 0029): their sample tables and codecs read, the
|
|
16
|
+
* lens layout and calibration decided, then one download a file, which the decode probe reads
|
|
17
|
+
* through first. Everything opened is released again when any step fails or the attempt is
|
|
18
|
+
* aborted.
|
|
15
19
|
*/
|
|
16
20
|
async function openInputs(inputs, attempt) {
|
|
17
21
|
const disposables = new Disposables();
|
|
18
22
|
try {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const calibration = calibrationOf(
|
|
23
|
+
const read = await readInputs(inputs, attempt);
|
|
24
|
+
const described = read.inputs.map((input) => describedInput(input));
|
|
25
|
+
const layout = detectLensLayout(described, read.recording.info);
|
|
26
|
+
const calibration = calibrationOf(read.recording);
|
|
27
|
+
const files = startDownloads(read.inputs, disposables);
|
|
28
|
+
const frameSources = lensFrameOrder(layout).map((key) => trackAt(files, key));
|
|
23
29
|
await ensureDecodable(frameSources, attempt);
|
|
24
|
-
const timing = await timeRecording(
|
|
30
|
+
const timing = await timeRecording(read.recording, frameSources[0]);
|
|
25
31
|
attempt.signal.throwIfAborted();
|
|
26
32
|
return assemble({
|
|
27
|
-
|
|
33
|
+
read,
|
|
34
|
+
files,
|
|
28
35
|
layout,
|
|
29
36
|
frameSources,
|
|
30
37
|
calibration,
|
|
31
38
|
timing
|
|
32
|
-
}, disposables);
|
|
39
|
+
}, attempt.ports.audioPackager, disposables);
|
|
33
40
|
} catch (error) {
|
|
34
41
|
disposables.disposeAll();
|
|
35
42
|
throw error;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
|
-
async function demuxInputs(inputs, attempt, disposables) {
|
|
39
|
-
const { ports, signal } = attempt;
|
|
40
|
-
const given = inputs.map((input) => ({
|
|
41
|
-
input,
|
|
42
|
-
source: ports.sources.open(input, signal)
|
|
43
|
-
}));
|
|
44
|
-
const recording = await readRecordingOf(given, ports);
|
|
45
|
-
signal.throwIfAborted();
|
|
46
|
-
const openings = [...given, ...await declaredSecond(given, recording, attempt)];
|
|
47
|
-
const settled = await Promise.allSettled(openings.map(({ input, source }) => ports.demuxer.open(source, inputName(input))));
|
|
48
|
-
const opened = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
49
|
-
for (const input of opened) disposables.add(() => {
|
|
50
|
-
input.dispose();
|
|
51
|
-
});
|
|
52
|
-
const failure = settled.find((result) => result.status === "rejected");
|
|
53
|
-
if (failure) throw failure.reason;
|
|
54
|
-
signal.throwIfAborted();
|
|
55
|
-
return {
|
|
56
|
-
recording,
|
|
57
|
-
inputs: opened
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* The other lens file a lone file's info record says the recording is split into, when the
|
|
62
|
-
* server has it: found before the lone file's tracks are read, which finding it only once they
|
|
63
|
-
* fell short would read twice. A file that does not say so still gets its sibling looked for
|
|
64
|
-
* once its tracks fall short (see openRecording).
|
|
65
|
-
*/
|
|
66
|
-
async function declaredSecond(given, recording, attempt) {
|
|
67
|
-
if (!(given.length === 1 && recording.info.fileLayout === "split-files") || !attempt.findSecondFile) return [];
|
|
68
|
-
const input = await attempt.findSecondFile();
|
|
69
|
-
attempt.signal.throwIfAborted();
|
|
70
|
-
return input === void 0 ? [] : [{
|
|
71
|
-
input,
|
|
72
|
-
source: attempt.ports.sources.open(input, attempt.signal)
|
|
73
|
-
}];
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* The layout hints of a file without an info record: the tracks alone decide.
|
|
77
|
-
*/
|
|
78
|
-
var NO_HINTS = {
|
|
79
|
-
fileLayout: void 0,
|
|
80
|
-
trackOrder: void 0
|
|
81
|
-
};
|
|
82
|
-
async function readRecordingOf(openings, ports) {
|
|
83
|
-
try {
|
|
84
|
-
return await readTrailerCarrier(openings.map(({ source }) => source));
|
|
85
|
-
} catch (error) {
|
|
86
|
-
const [only] = openings;
|
|
87
|
-
if (openings.length === 1 && hasErrorCode(error, "invalid-trailer") && only) await refuseAsLoneHalf(only, ports);
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
45
|
/**
|
|
92
|
-
*
|
|
93
|
-
* trailer to _00_ alone: its one square track tells, and the refusal asks for the other file
|
|
94
|
-
* instead of calling it damaged. Anything else leaves the trailer's own refusal standing.
|
|
46
|
+
* One download a file, each with its share of the budget.
|
|
95
47
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* first has none, as the _10_ file of an older camera's pair, which writes it to _00_ alone,
|
|
114
|
-
* when the pair is given the other way round.
|
|
115
|
-
*/
|
|
116
|
-
async function readTrailerCarrier(sources) {
|
|
117
|
-
const [first, second] = sources;
|
|
118
|
-
ensureInvariant(first !== void 0, "a recording needs at least one input");
|
|
119
|
-
try {
|
|
120
|
-
return await readRecording(first);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
if (second === void 0 || !hasErrorCode(error, "invalid-trailer")) throw error;
|
|
123
|
-
return readRecording(second);
|
|
124
|
-
}
|
|
48
|
+
function startDownloads(inputs, disposables) {
|
|
49
|
+
return inputs.map(({ opened, table, codecs, size }) => {
|
|
50
|
+
const policy = downloadPolicyFor({
|
|
51
|
+
size,
|
|
52
|
+
duration: table.duration
|
|
53
|
+
}, inputs.length);
|
|
54
|
+
const file = startFileDownload({
|
|
55
|
+
table,
|
|
56
|
+
stream: opened.stream,
|
|
57
|
+
codecs,
|
|
58
|
+
policy
|
|
59
|
+
});
|
|
60
|
+
disposables.add(() => {
|
|
61
|
+
file.dispose();
|
|
62
|
+
});
|
|
63
|
+
return file;
|
|
64
|
+
});
|
|
125
65
|
}
|
|
126
|
-
function trackAt(
|
|
127
|
-
const track =
|
|
66
|
+
function trackAt(files, key) {
|
|
67
|
+
const track = files[key.inputIndex]?.videoTracks[key.trackIndex];
|
|
128
68
|
ensureInvariant(track !== void 0, `the layout points at track ${key.trackIndex} of input ${key.inputIndex}, which does not exist`);
|
|
129
69
|
return track;
|
|
130
70
|
}
|
|
@@ -134,26 +74,30 @@ function calibrationOf(recording) {
|
|
|
134
74
|
const detail = warnings.length > 0 ? ` (${warnings.join("; ")})` : "";
|
|
135
75
|
throw new GyroViewError("no-calibration", `the recording carries no usable lens calibration, so it cannot be stitched${detail}`);
|
|
136
76
|
}
|
|
137
|
-
function assemble(parts, disposables) {
|
|
138
|
-
const {
|
|
139
|
-
const duration = seconds(Math.min(...
|
|
140
|
-
const [
|
|
77
|
+
function assemble(parts, packager, disposables) {
|
|
78
|
+
const { read, files, layout, frameSources, calibration, timing } = parts;
|
|
79
|
+
const duration = seconds(Math.min(...files.map((file) => file.duration)));
|
|
80
|
+
const [sound] = files.flatMap((file) => file.audioTracks);
|
|
141
81
|
return {
|
|
142
|
-
recording:
|
|
82
|
+
recording: read.recording,
|
|
143
83
|
layout,
|
|
144
84
|
frameSources,
|
|
145
85
|
calibration,
|
|
146
86
|
duration,
|
|
147
87
|
frameTimes: timing.frameTimes,
|
|
148
88
|
motion: timing.motion,
|
|
149
|
-
|
|
150
|
-
metadata: metadataOf(parts, duration,
|
|
89
|
+
soundSegments: sound && (() => packager.segmentsOf(sound.samples, sound.configuration)),
|
|
90
|
+
metadata: metadataOf(parts, duration, sound !== void 0),
|
|
151
91
|
warnings: timing.warnings,
|
|
92
|
+
readAhead: () => {
|
|
93
|
+
for (const file of files) file.download.startReadingAhead();
|
|
94
|
+
},
|
|
95
|
+
buffer: new RecordingBuffer(files.map((file) => file.download)),
|
|
152
96
|
dispose: disposables.toDisposer()
|
|
153
97
|
};
|
|
154
98
|
}
|
|
155
99
|
function metadataOf(parts, duration, hasAudio) {
|
|
156
|
-
const { info } = parts.
|
|
100
|
+
const { info } = parts.read.recording;
|
|
157
101
|
const { motion } = parts.timing;
|
|
158
102
|
return {
|
|
159
103
|
model: info.model,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ensureInvariant, hasErrorCode } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import { readRecording } from "../../../core/src/application/recording/readRecording.js";
|
|
3
|
+
import { readSampleTable } from "../../../core/src/application/recording/readSampleTable.js";
|
|
4
|
+
import { detectLensLayout } from "../../../core/src/domain/format/layout/detectLensLayout.js";
|
|
5
|
+
import "../../../core/src/index.js";
|
|
6
|
+
import { inputName } from "../PlayerSource.js";
|
|
7
|
+
//#region ../../packages/player/src/composition/readInputs.ts
|
|
8
|
+
/**
|
|
9
|
+
* Reads the given inputs, and the other lens file an info record declares, as one recording: its
|
|
10
|
+
* metadata from the input that carries the trailer, and every input's sample table and codecs.
|
|
11
|
+
*/
|
|
12
|
+
async function readInputs(inputs, attempt) {
|
|
13
|
+
const { ports, signal } = attempt;
|
|
14
|
+
const given = inputs.map((input) => ({
|
|
15
|
+
input,
|
|
16
|
+
opened: ports.sources.open(input, signal)
|
|
17
|
+
}));
|
|
18
|
+
const recording = await readRecordingOf(given, ports);
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
const openings = [...given, ...await declaredSecond(given, recording, attempt)];
|
|
21
|
+
const read = await Promise.all(openings.map((opening) => readInput(opening, ports)));
|
|
22
|
+
signal.throwIfAborted();
|
|
23
|
+
return {
|
|
24
|
+
recording,
|
|
25
|
+
inputs: read
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function describedInput(read) {
|
|
29
|
+
const videoTracks = read.codecs.video.map(({ description }) => ({ description }));
|
|
30
|
+
return {
|
|
31
|
+
name: inputName(read.input),
|
|
32
|
+
videoTracks
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function readInput(opening, ports) {
|
|
36
|
+
const { source } = opening.opened;
|
|
37
|
+
const { table, movieBytes } = await readSampleTable(source);
|
|
38
|
+
const codecs = await ports.codecReader.read(movieBytes);
|
|
39
|
+
return {
|
|
40
|
+
...opening,
|
|
41
|
+
table,
|
|
42
|
+
codecs,
|
|
43
|
+
size: await source.size()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The other lens file a lone file's info record says the recording is split into, when the
|
|
48
|
+
* server has it: found before the lone file's tracks are read, which finding it only once they
|
|
49
|
+
* fell short would read twice. A file that does not say so still gets its sibling looked for
|
|
50
|
+
* once its tracks fall short (see openRecording).
|
|
51
|
+
*/
|
|
52
|
+
async function declaredSecond(given, recording, attempt) {
|
|
53
|
+
if (!(given.length === 1 && recording.info.fileLayout === "split-files") || !attempt.findSecondFile) return [];
|
|
54
|
+
const input = await attempt.findSecondFile();
|
|
55
|
+
attempt.signal.throwIfAborted();
|
|
56
|
+
return input === void 0 ? [] : [{
|
|
57
|
+
input,
|
|
58
|
+
opened: attempt.ports.sources.open(input, attempt.signal)
|
|
59
|
+
}];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The layout hints of a file without an info record: the tracks alone decide.
|
|
63
|
+
*/
|
|
64
|
+
var NO_HINTS = {
|
|
65
|
+
fileLayout: void 0,
|
|
66
|
+
trackOrder: void 0
|
|
67
|
+
};
|
|
68
|
+
async function readRecordingOf(openings, ports) {
|
|
69
|
+
try {
|
|
70
|
+
return await readTrailerCarrier(openings.map(({ opened }) => opened.source));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const [only] = openings;
|
|
73
|
+
if (openings.length === 1 && hasErrorCode(error, "invalid-trailer") && only) await refuseAsLoneHalf(only, ports);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A file without a trailer may be the _10_ half of an older camera's pair, which writes the
|
|
79
|
+
* trailer to _00_ alone: its one square track tells, and the refusal asks for the other file
|
|
80
|
+
* instead of calling it damaged. Anything else leaves the trailer's own refusal standing.
|
|
81
|
+
*/
|
|
82
|
+
async function refuseAsLoneHalf(opening, ports) {
|
|
83
|
+
let read;
|
|
84
|
+
try {
|
|
85
|
+
read = await readInput(opening, ports);
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
detectLensLayout([describedInput(read)], NO_HINTS);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (hasErrorCode(error, "missing-second-file")) throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The recording read from the input that carries the trailer: the first, or the second when the
|
|
97
|
+
* first has none, as the _10_ file of an older camera's pair, which writes it to _00_ alone,
|
|
98
|
+
* when the pair is given the other way round.
|
|
99
|
+
*/
|
|
100
|
+
async function readTrailerCarrier(sources) {
|
|
101
|
+
const [first, second] = sources;
|
|
102
|
+
ensureInvariant(first !== void 0, "a recording needs at least one input");
|
|
103
|
+
try {
|
|
104
|
+
return await readRecording(first);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (second === void 0 || !hasErrorCode(error, "invalid-trailer")) throw error;
|
|
107
|
+
return readRecording(second);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { describedInput, readInputs };
|
|
@@ -71,6 +71,7 @@ var DEFAULT_MESSAGES = {
|
|
|
71
71
|
"playback-blocked": FAILED,
|
|
72
72
|
"range-unsupported": UNREACHABLE,
|
|
73
73
|
"render-unavailable": UNSUPPORTED_BROWSER,
|
|
74
|
+
"source-changed": "The video was replaced while it played.",
|
|
74
75
|
"source-truncated": UNREACHABLE,
|
|
75
76
|
"source-unreadable": UNREACHABLE,
|
|
76
77
|
"unsupported-calibration": UNREADABLE,
|
|
@@ -9,7 +9,7 @@ import { browserSources } from "./composition/browserPorts.js";
|
|
|
9
9
|
*/
|
|
10
10
|
function inspectRecording(recording, options = {}) {
|
|
11
11
|
const signal = options.signal ?? new AbortController().signal;
|
|
12
|
-
const source = browserSources(options.http ?? {}).open(inputOf(recording), signal);
|
|
12
|
+
const { source } = browserSources(options.http ?? {}).open(inputOf(recording), signal);
|
|
13
13
|
return inspectRecording$1(source);
|
|
14
14
|
}
|
|
15
15
|
function inputOf(recording) {
|