@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
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { SourceByteStream } from "../../../core/src/application/download/SourceByteStream.js";
|
|
1
2
|
import { Deferred } from "../../../core/src/shared/async/Deferred.js";
|
|
2
3
|
import "../../../core/src/index.js";
|
|
3
4
|
import { BlobRandomAccessSource } from "../../../adapters/blob/src/BlobRandomAccessSource.js";
|
|
4
5
|
import "../../../adapters/blob/src/index.js";
|
|
6
|
+
import { HttpByteStream } from "../../../adapters/fetch/src/HttpByteStream.js";
|
|
5
7
|
import { HttpRangeSource } from "../../../adapters/fetch/src/HttpRangeSource.js";
|
|
8
|
+
import { HttpResource } from "../../../adapters/fetch/src/HttpResource.js";
|
|
6
9
|
import { HttpResourceLocator } from "../../../adapters/fetch/src/HttpResourceLocator.js";
|
|
7
10
|
import "../../../adapters/fetch/src/index.js";
|
|
8
|
-
import {
|
|
11
|
+
import { MediabunnyCodecReader } from "../../../adapters/mediabunny/src/MediabunnyCodecReader.js";
|
|
12
|
+
import { MediabunnyAudioPackager } from "../../../adapters/mediabunny/src/MediabunnyAudioPackager.js";
|
|
9
13
|
import "../../../adapters/mediabunny/src/index.js";
|
|
10
14
|
import { WebCodecsVideoDecoderPort } from "../../../adapters/webcodecs/src/WebCodecsVideoDecoderPort.js";
|
|
11
15
|
import "../../../adapters/webcodecs/src/index.js";
|
|
@@ -20,17 +24,46 @@ function browserPorts(options = {}) {
|
|
|
20
24
|
const probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
21
25
|
return {
|
|
22
26
|
sources: browserSources(http),
|
|
23
|
-
|
|
27
|
+
codecReader: new MediabunnyCodecReader(),
|
|
28
|
+
audioPackager: new MediabunnyAudioPackager(),
|
|
24
29
|
decoderPort: new WebCodecsVideoDecoderPort(),
|
|
25
|
-
|
|
30
|
+
locatorFor: (input) => new HttpResourceLocator(requestOptionsFor(http, input)),
|
|
26
31
|
probeDeadline: () => deadlineIn(probeTimeoutMs)
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
|
-
* URLs are read with HTTP ranges, blobs by slicing.
|
|
35
|
+
* URLs are read with HTTP ranges, both ways over one resource; blobs by slicing, a range whole.
|
|
31
36
|
*/
|
|
32
37
|
function browserSources(http) {
|
|
33
|
-
return { open: (input, signal) => isUrlInput(input) ?
|
|
38
|
+
return { open: (input, signal) => isUrlInput(input) ? urlSource(input, http, signal) : blobSource(input) };
|
|
39
|
+
}
|
|
40
|
+
function urlSource(input, http, signal) {
|
|
41
|
+
const resource = new HttpResource(input.url, requestOptionsFor(http, input), signal);
|
|
42
|
+
return {
|
|
43
|
+
source: new HttpRangeSource(resource),
|
|
44
|
+
stream: new HttpByteStream(resource)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function blobSource(input) {
|
|
48
|
+
const source = new BlobRandomAccessSource(input.blob);
|
|
49
|
+
return {
|
|
50
|
+
source,
|
|
51
|
+
stream: new SourceByteStream(source)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The shared request settings, with the input's own credentials in place of theirs when it names
|
|
56
|
+
* any: how the element's `crossorigin` reaches every request for its recording (ADR 0027).
|
|
57
|
+
*/
|
|
58
|
+
function requestOptionsFor(http, input) {
|
|
59
|
+
const { credentials } = input;
|
|
60
|
+
return credentials === void 0 ? http : {
|
|
61
|
+
...http,
|
|
62
|
+
requestInit: {
|
|
63
|
+
...http.requestInit,
|
|
64
|
+
credentials
|
|
65
|
+
}
|
|
66
|
+
};
|
|
34
67
|
}
|
|
35
68
|
/**
|
|
36
69
|
* A deadline that passes after `ms`: the core has no timers, so the host supplies deadlines.
|
|
@@ -5,6 +5,7 @@ import { buildStitchingSetup } from "../../../core/src/domain/stitching/Stitchin
|
|
|
5
5
|
import { StabilizingFrameSink } from "../../../core/src/application/stabilization/StabilizingFrameSink.js";
|
|
6
6
|
import { GainMatchingFrameSink } from "../../../core/src/application/gainMatching/GainMatchingFrameSink.js";
|
|
7
7
|
import { WallClock } from "../../../core/src/application/playback/WallClock.js";
|
|
8
|
+
import { isFlowing } from "../../../core/src/domain/playback/PlayerState.js";
|
|
8
9
|
import { PlaybackSession } from "../../../core/src/application/playback/PlaybackSession.js";
|
|
9
10
|
import "../../../core/src/index.js";
|
|
10
11
|
import { MediaSourceAudioClock } from "../../../adapters/mse-audio/src/MediaSourceAudioClock.js";
|
|
@@ -37,6 +38,7 @@ async function buildPipeline(parts) {
|
|
|
37
38
|
disposables.add(() => {
|
|
38
39
|
session.dispose();
|
|
39
40
|
});
|
|
41
|
+
readAheadOnceFlowing(session, parts.opened);
|
|
40
42
|
return {
|
|
41
43
|
session,
|
|
42
44
|
renderer: drawing.renderer,
|
|
@@ -94,10 +96,10 @@ function pictureSettingsOf(drawing, session) {
|
|
|
94
96
|
* otherwise a wall clock, with a warning saying why there is no sound.
|
|
95
97
|
*/
|
|
96
98
|
async function clockFor(parts) {
|
|
97
|
-
const {
|
|
98
|
-
if (!
|
|
99
|
+
const { soundSegments } = parts.opened;
|
|
100
|
+
if (!soundSegments) return wallClock(NO_AUDIO_WARNING);
|
|
99
101
|
try {
|
|
100
|
-
const segments =
|
|
102
|
+
const segments = soundSegments();
|
|
101
103
|
if (!MediaSourceAudioClock.isSupported(segments)) return wallClock(AUDIO_UNSUPPORTED_WARNING);
|
|
102
104
|
parts.signal.throwIfAborted();
|
|
103
105
|
return {
|
|
@@ -138,6 +140,15 @@ function sinkOver(renderer, opened) {
|
|
|
138
140
|
stabilizing
|
|
139
141
|
};
|
|
140
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* The downloads read only what the picture waits for until playing first starts, so a page of
|
|
145
|
+
* players that were never played stays quiet; from then on they read ahead, paused or not.
|
|
146
|
+
*/
|
|
147
|
+
function readAheadOnceFlowing(session, opened) {
|
|
148
|
+
session.events.on("statechange", (state) => {
|
|
149
|
+
if (isFlowing(state)) opened.readAhead();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
141
152
|
function sessionFor(parts, clock, sink) {
|
|
142
153
|
return new PlaybackSession({
|
|
143
154
|
frameSources: parts.opened.frameSources,
|
|
@@ -145,6 +156,7 @@ function sessionFor(parts, clock, sink) {
|
|
|
145
156
|
clock,
|
|
146
157
|
sink,
|
|
147
158
|
duration: parts.opened.duration,
|
|
159
|
+
buffer: parts.opened.buffer,
|
|
148
160
|
pipeline: DECODE_PIPELINE_OPTIONS,
|
|
149
161
|
queueCapacity: 4
|
|
150
162
|
});
|
|
@@ -1,132 +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 url = await attempt.findSecondFile();
|
|
69
|
-
attempt.signal.throwIfAborted();
|
|
70
|
-
if (url === void 0) return [];
|
|
71
|
-
const input = { url };
|
|
72
|
-
return [{
|
|
73
|
-
input,
|
|
74
|
-
source: attempt.ports.sources.open(input, attempt.signal)
|
|
75
|
-
}];
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* The layout hints of a file without an info record: the tracks alone decide.
|
|
79
|
-
*/
|
|
80
|
-
var NO_HINTS = {
|
|
81
|
-
fileLayout: void 0,
|
|
82
|
-
trackOrder: void 0
|
|
83
|
-
};
|
|
84
|
-
async function readRecordingOf(openings, ports) {
|
|
85
|
-
try {
|
|
86
|
-
return await readTrailerCarrier(openings.map(({ source }) => source));
|
|
87
|
-
} catch (error) {
|
|
88
|
-
const [only] = openings;
|
|
89
|
-
if (openings.length === 1 && hasErrorCode(error, "invalid-trailer") && only) await refuseAsLoneHalf(only, ports);
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
45
|
/**
|
|
94
|
-
*
|
|
95
|
-
* trailer to _00_ alone: its one square track tells, and the refusal asks for the other file
|
|
96
|
-
* 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.
|
|
97
47
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* first has none, as the _10_ file of an older camera's pair, which writes it to _00_ alone,
|
|
116
|
-
* when the pair is given the other way round.
|
|
117
|
-
*/
|
|
118
|
-
async function readTrailerCarrier(sources) {
|
|
119
|
-
const [first, second] = sources;
|
|
120
|
-
ensureInvariant(first !== void 0, "a recording needs at least one input");
|
|
121
|
-
try {
|
|
122
|
-
return await readRecording(first);
|
|
123
|
-
} catch (error) {
|
|
124
|
-
if (second === void 0 || !hasErrorCode(error, "invalid-trailer")) throw error;
|
|
125
|
-
return readRecording(second);
|
|
126
|
-
}
|
|
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
|
+
});
|
|
127
65
|
}
|
|
128
|
-
function trackAt(
|
|
129
|
-
const track =
|
|
66
|
+
function trackAt(files, key) {
|
|
67
|
+
const track = files[key.inputIndex]?.videoTracks[key.trackIndex];
|
|
130
68
|
ensureInvariant(track !== void 0, `the layout points at track ${key.trackIndex} of input ${key.inputIndex}, which does not exist`);
|
|
131
69
|
return track;
|
|
132
70
|
}
|
|
@@ -136,26 +74,30 @@ function calibrationOf(recording) {
|
|
|
136
74
|
const detail = warnings.length > 0 ? ` (${warnings.join("; ")})` : "";
|
|
137
75
|
throw new GyroViewError("no-calibration", `the recording carries no usable lens calibration, so it cannot be stitched${detail}`);
|
|
138
76
|
}
|
|
139
|
-
function assemble(parts, disposables) {
|
|
140
|
-
const {
|
|
141
|
-
const duration = seconds(Math.min(...
|
|
142
|
-
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);
|
|
143
81
|
return {
|
|
144
|
-
recording:
|
|
82
|
+
recording: read.recording,
|
|
145
83
|
layout,
|
|
146
84
|
frameSources,
|
|
147
85
|
calibration,
|
|
148
86
|
duration,
|
|
149
87
|
frameTimes: timing.frameTimes,
|
|
150
88
|
motion: timing.motion,
|
|
151
|
-
|
|
152
|
-
metadata: metadataOf(parts, duration,
|
|
89
|
+
soundSegments: sound && (() => packager.segmentsOf(sound.samples, sound.configuration)),
|
|
90
|
+
metadata: metadataOf(parts, duration, sound !== void 0),
|
|
153
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)),
|
|
154
96
|
dispose: disposables.toDisposer()
|
|
155
97
|
};
|
|
156
98
|
}
|
|
157
99
|
function metadataOf(parts, duration, hasAudio) {
|
|
158
|
-
const { info } = parts.
|
|
100
|
+
const { info } = parts.read.recording;
|
|
159
101
|
const { motion } = parts.timing;
|
|
160
102
|
return {
|
|
161
103
|
model: info.model,
|
|
@@ -21,7 +21,7 @@ async function openRecording(source, ports, signal) {
|
|
|
21
21
|
const second = await findSecondFile();
|
|
22
22
|
signal.throwIfAborted();
|
|
23
23
|
if (!second) throw error;
|
|
24
|
-
return openInputs([source.main,
|
|
24
|
+
return openInputs([source.main, second], attempt);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
@@ -32,11 +32,22 @@ function attemptFor(source, ports, signal) {
|
|
|
32
32
|
return source.second === void 0 && isUrlInput(main) ? {
|
|
33
33
|
ports,
|
|
34
34
|
signal,
|
|
35
|
-
findSecondFile: lazy(() =>
|
|
35
|
+
findSecondFile: lazy(() => otherLensFileOf(main, ports))
|
|
36
36
|
} : {
|
|
37
37
|
ports,
|
|
38
38
|
signal
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The other lens file beside a lone one, when the server has it: one recording, so it is read
|
|
43
|
+
* as the lone file is, with the same credentials.
|
|
44
|
+
*/
|
|
45
|
+
async function otherLensFileOf(main, ports) {
|
|
46
|
+
const url = await locateOtherLensFile(main.url, ports.locatorFor(main));
|
|
47
|
+
return url === void 0 ? void 0 : {
|
|
48
|
+
...main,
|
|
49
|
+
url
|
|
50
|
+
};
|
|
51
|
+
}
|
|
41
52
|
//#endregion
|
|
42
53
|
export { openRecording };
|
|
@@ -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 };
|
|
@@ -14,22 +14,27 @@ var FOCUS_STEPS = /* @__PURE__ */ new Map([
|
|
|
14
14
|
var ON_THE_WAY_IN = { capture: true };
|
|
15
15
|
/**
|
|
16
16
|
* A button opening a popup of choices with the current one checked, as the ARIA menu pattern
|
|
17
|
-
* has it. The button toggles it; a choice,
|
|
18
|
-
* elsewhere closes it; the arrow keys move between the choices, which are out of the tab order.
|
|
17
|
+
* has it; the button shows the checked one. The button toggles it; a choice, its close button,
|
|
18
|
+
* Escape, Tab, a press outside it or the focus moving elsewhere closes it; the arrow keys move between the choices, which are out of the tab order.
|
|
19
19
|
* A press outside only closes it, so a tap on the picture does not also toggle play, and keys
|
|
20
20
|
* pressed in it stay with it, so one Escape does not also leave fullscreen and the arrows do not
|
|
21
21
|
* turn the view.
|
|
22
22
|
*/
|
|
23
23
|
var ChoiceMenu = class {
|
|
24
24
|
parts;
|
|
25
|
+
host;
|
|
25
26
|
items;
|
|
26
|
-
constructor(root, parts,
|
|
27
|
+
constructor(root, parts, host) {
|
|
27
28
|
this.parts = parts;
|
|
29
|
+
this.host = host;
|
|
28
30
|
this.items = [...parts.popup.querySelectorAll(":scope [role=\"menuitemradio\"]")];
|
|
29
31
|
for (const item of this.items) item.addEventListener("click", () => {
|
|
30
32
|
this.closeToButton();
|
|
31
33
|
const { choice } = item.dataset;
|
|
32
|
-
if (choice !== void 0) choose(choice);
|
|
34
|
+
if (choice !== void 0) host.choose(choice);
|
|
35
|
+
});
|
|
36
|
+
parts.close.addEventListener("click", () => {
|
|
37
|
+
this.closeToButton();
|
|
33
38
|
});
|
|
34
39
|
this.bindButton();
|
|
35
40
|
this.bindDismissal(root);
|
|
@@ -38,8 +43,15 @@ var ChoiceMenu = class {
|
|
|
38
43
|
get isOpen() {
|
|
39
44
|
return !this.parts.popup.hidden;
|
|
40
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks `choice` and shows it on the button: its icon, and the words the button is named by.
|
|
48
|
+
*/
|
|
41
49
|
markChosen(choice) {
|
|
42
50
|
for (const item of this.items) item.setAttribute("aria-checked", String(item.dataset["choice"] === choice));
|
|
51
|
+
const icon = this.items.find((item) => item.dataset["choice"] === choice)?.querySelector(":scope .choice-icon > svg");
|
|
52
|
+
this.parts.icon.replaceChildren(...icon ? [icon.cloneNode(true)] : []);
|
|
53
|
+
this.parts.button.dataset["choice"] = choice;
|
|
54
|
+
this.host.wording.write(this.parts.button);
|
|
43
55
|
}
|
|
44
56
|
/**
|
|
45
57
|
* Shows or hides the button; a menu that goes away closes.
|
|
@@ -10,19 +10,33 @@ import { ChoiceMenu } from "./ChoiceMenu.js";
|
|
|
10
10
|
* shown in a stitched view mode.
|
|
11
11
|
*/
|
|
12
12
|
var PictureMenus = class {
|
|
13
|
-
player;
|
|
14
13
|
viewMode;
|
|
15
14
|
stabilization;
|
|
16
|
-
|
|
15
|
+
player;
|
|
16
|
+
constructor(root, parts, host) {
|
|
17
|
+
const { player, wording } = host;
|
|
17
18
|
this.player = player;
|
|
18
|
-
this.viewMode = new ChoiceMenu(root, parts.viewMode,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
this.viewMode = new ChoiceMenu(root, parts.viewMode, {
|
|
20
|
+
wording,
|
|
21
|
+
choose: (choice) => {
|
|
22
|
+
const mode = viewModeOf(choice);
|
|
23
|
+
if (mode) player.setViewMode(mode);
|
|
24
|
+
}
|
|
21
25
|
});
|
|
22
|
-
this.stabilization = new ChoiceMenu(root, parts.stabilization,
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
this.stabilization = new ChoiceMenu(root, parts.stabilization, {
|
|
27
|
+
wording,
|
|
28
|
+
choose: (choice) => {
|
|
29
|
+
const mode = stabilizationModeOf(choice);
|
|
30
|
+
if (mode) player.setStabilization(mode);
|
|
31
|
+
}
|
|
25
32
|
});
|
|
33
|
+
this.followPlayer();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Each menu checks the mode in effect, now and whenever it changes.
|
|
37
|
+
*/
|
|
38
|
+
followPlayer() {
|
|
39
|
+
const { player } = this;
|
|
26
40
|
player.events.on("viewmodechange", (mode) => {
|
|
27
41
|
this.viewMode.markChosen(mode);
|
|
28
42
|
this.offerStabilization();
|
|
@@ -2,6 +2,7 @@ import { messageOf } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
|
2
2
|
import "../../../core/src/index.js";
|
|
3
3
|
import "./keyboard.js";
|
|
4
4
|
import { formatTime } from "./formatTime.js";
|
|
5
|
+
import { showSliderFill } from "./sliderFill.js";
|
|
5
6
|
//#region ../../packages/player/src/controls/SeekBar.ts
|
|
6
7
|
/**
|
|
7
8
|
* The slider's arrow keys and the direction each seeks in: the native step is the one the
|
|
@@ -110,15 +111,17 @@ var SeekBar = class {
|
|
|
110
111
|
this.host.player.seek(Number(this.parts.seek.value));
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
113
|
-
* The time beside the bar, and the same time for screen readers,
|
|
114
|
-
* raw seconds.
|
|
114
|
+
* The time beside the bar and the bar filled up to it, and the same time for screen readers,
|
|
115
|
+
* which would read the slider's raw seconds.
|
|
115
116
|
*/
|
|
116
117
|
showTime(time) {
|
|
117
118
|
const [shown, total] = [formatTime(time), formatTime(this.host.player.duration)];
|
|
118
|
-
this.parts.
|
|
119
|
+
this.parts.elapsed.textContent = shown;
|
|
120
|
+
this.parts.total.textContent = `/ ${total}`;
|
|
119
121
|
const { position } = this.host.wording.current.labels;
|
|
120
122
|
const spoken = position.replaceAll("{time}", () => shown).replaceAll("{duration}", () => total);
|
|
121
123
|
this.parts.seek.setAttribute("aria-valuetext", spoken);
|
|
124
|
+
showSliderFill(this.parts.seek);
|
|
122
125
|
}
|
|
123
126
|
};
|
|
124
127
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { iconNode } from "./icons.js";
|
|
2
|
+
import { showSliderFill } from "./sliderFill.js";
|
|
2
3
|
//#region ../../packages/player/src/controls/SoundControls.ts
|
|
3
4
|
var PERCENT = 100;
|
|
4
5
|
/**
|
|
@@ -35,6 +36,7 @@ var SoundControls = class {
|
|
|
35
36
|
this.parts.mute.setAttribute("aria-pressed", String(isMuted));
|
|
36
37
|
this.parts.volume.value = String(shown);
|
|
37
38
|
this.parts.volume.setAttribute("aria-valuetext", `${Math.round(shown * PERCENT)}%`);
|
|
39
|
+
showSliderFill(this.parts.volume);
|
|
38
40
|
}
|
|
39
41
|
};
|
|
40
42
|
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { iconNode } from "./icons.js";
|
|
2
2
|
//#region ../../packages/player/src/controls/TransportButtons.ts
|
|
3
3
|
/**
|
|
4
|
-
* Play and pause, on the bar and over the picture
|
|
5
|
-
*
|
|
4
|
+
* Play and pause, on the bar and over the picture; the buttons say what a press does. Stopping
|
|
5
|
+
* is left to the S key and `stop()`.
|
|
6
6
|
*/
|
|
7
7
|
var TransportButtons = class {
|
|
8
8
|
parts;
|
|
@@ -14,9 +14,6 @@ var TransportButtons = class {
|
|
|
14
14
|
for (const button of [parts.play, parts.bigPlay]) button.addEventListener("click", () => {
|
|
15
15
|
host.togglePlay();
|
|
16
16
|
});
|
|
17
|
-
parts.stop.addEventListener("click", () => {
|
|
18
|
-
player.stop();
|
|
19
|
-
});
|
|
20
17
|
player.events.on("statuschange", () => {
|
|
21
18
|
this.reflect(player.isPaused);
|
|
22
19
|
});
|