@bubo-squared/gyroview 0.1.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 +32 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +4 -0
- package/dist/index.d.ts +959 -0
- package/dist/index.js +13 -0
- package/dist/packages/adapters/blob/src/BlobRandomAccessSource.js +37 -0
- package/dist/packages/adapters/blob/src/index.js +2 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +194 -0
- package/dist/packages/adapters/fetch/src/HttpResourceLocator.js +35 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +126 -0
- package/dist/packages/adapters/fetch/src/index.js +3 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +133 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +63 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +84 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +84 -0
- package/dist/packages/adapters/mediabunny/src/bufferSources.js +12 -0
- package/dist/packages/adapters/mediabunny/src/index.js +2 -0
- package/dist/packages/adapters/mse-audio/src/MediaSourceAudioClock.js +139 -0
- package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +174 -0
- package/dist/packages/adapters/mse-audio/src/events.js +26 -0
- package/dist/packages/adapters/mse-audio/src/index.js +2 -0
- package/dist/packages/adapters/mse-audio/src/mediaSourceSupport.js +82 -0
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +274 -0
- package/dist/packages/adapters/three/src/compileAndProve.js +14 -0
- package/dist/packages/adapters/three/src/fullscreenPass.js +52 -0
- package/dist/packages/adapters/three/src/index.js +2 -0
- package/dist/packages/adapters/three/src/lensTextures.js +25 -0
- package/dist/packages/adapters/three/src/pictureMaterials.js +44 -0
- package/dist/packages/adapters/three/src/renderInto.js +15 -0
- package/dist/packages/adapters/three/src/rendererUniforms.js +157 -0
- package/dist/packages/adapters/three/src/samplingStrategies.js +26 -0
- package/dist/packages/adapters/three/src/seamMeter/SeamMeterPass.js +79 -0
- package/dist/packages/adapters/three/src/seamMeter/rowMeans.js +38 -0
- package/dist/packages/adapters/three/src/shaderPrograms.js +66 -0
- package/dist/packages/adapters/three/src/shaders/analysis.frag.js +4 -0
- package/dist/packages/adapters/three/src/shaders/equirectangularRays.js +4 -0
- package/dist/packages/adapters/three/src/shaders/fixedJoin.js +4 -0
- package/dist/packages/adapters/three/src/shaders/fullscreen.vert.js +4 -0
- package/dist/packages/adapters/three/src/shaders/header.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensModels.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensSampling.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensTextures.js +4 -0
- package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +4 -0
- package/dist/packages/adapters/three/src/shaders/rectilinearRays.js +4 -0
- package/dist/packages/adapters/three/src/shaders/screenAreas.js +4 -0
- package/dist/packages/adapters/three/src/shaders/stitch.frag.js +4 -0
- package/dist/packages/adapters/three/src/webglRenderer.js +37 -0
- package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +135 -0
- package/dist/packages/adapters/webcodecs/src/index.js +2 -0
- package/dist/packages/core/src/application/gainMatching/GainMatching.js +66 -0
- package/dist/packages/core/src/application/gainMatching/GainMatchingFrameSink.js +62 -0
- package/dist/packages/core/src/application/playback/ClockWatch.js +76 -0
- package/dist/packages/core/src/application/playback/DecodePipeline.js +188 -0
- package/dist/packages/core/src/application/playback/DecodeRun.js +93 -0
- package/dist/packages/core/src/application/playback/FramePairQueue.js +93 -0
- package/dist/packages/core/src/application/playback/FramePairer.js +60 -0
- package/dist/packages/core/src/application/playback/PlaybackSession.js +417 -0
- package/dist/packages/core/src/application/playback/SeekOrder.js +36 -0
- package/dist/packages/core/src/application/playback/SessionLifecycle.js +42 -0
- package/dist/packages/core/src/application/playback/StartGate.js +55 -0
- package/dist/packages/core/src/application/playback/TimeUpdates.js +31 -0
- package/dist/packages/core/src/application/playback/WallClock.js +46 -0
- package/dist/packages/core/src/application/playback/keyframeTimeAt.js +15 -0
- package/dist/packages/core/src/application/playback/probeDecoding.js +168 -0
- package/dist/packages/core/src/application/recording/frameTimesOf.js +22 -0
- package/dist/packages/core/src/application/recording/inspectLayout.js +21 -0
- package/dist/packages/core/src/application/recording/inspectRecording.js +124 -0
- package/dist/packages/core/src/application/recording/locateOtherLensFile.js +18 -0
- package/dist/packages/core/src/application/recording/motionOf.js +39 -0
- package/dist/packages/core/src/application/recording/readRecording.js +31 -0
- package/dist/packages/core/src/application/recording/timeRecording.js +57 -0
- package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +31 -0
- package/dist/packages/core/src/domain/format/boxes/BoxLayout.js +7 -0
- package/dist/packages/core/src/domain/format/boxes/boxConstants.js +16 -0
- package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +59 -0
- package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +13 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +34 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +44 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +45 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +44 -0
- package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +65 -0
- package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +52 -0
- package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +46 -0
- package/dist/packages/core/src/domain/format/captureOrigin.js +20 -0
- package/dist/packages/core/src/domain/format/constants.js +33 -0
- package/dist/packages/core/src/domain/format/info/infoFields.js +110 -0
- package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +90 -0
- package/dist/packages/core/src/domain/format/info/readInfoRecord.js +14 -0
- package/dist/packages/core/src/domain/format/layout/detectLensLayout.js +122 -0
- package/dist/packages/core/src/domain/format/naming/RecordingFileName.js +67 -0
- package/dist/packages/core/src/domain/format/records/TrailerRecords.js +83 -0
- package/dist/packages/core/src/domain/format/records/exposure/parseExposureRecord.js +48 -0
- package/dist/packages/core/src/domain/format/records/gyro/FloatGyroSampleLayout.js +39 -0
- package/dist/packages/core/src/domain/format/records/gyro/RawGyroSampleLayout.js +55 -0
- package/dist/packages/core/src/domain/format/records/gyro/SampleColumns.js +82 -0
- package/dist/packages/core/src/domain/format/records/gyro/gyroLayouts.js +12 -0
- package/dist/packages/core/src/domain/format/records/gyro/parseGyroRecord.js +129 -0
- package/dist/packages/core/src/domain/format/trailer/RecordHeader.js +23 -0
- package/dist/packages/core/src/domain/format/trailer/Trailer.js +23 -0
- package/dist/packages/core/src/domain/format/trailer/TrailerFooter.js +27 -0
- package/dist/packages/core/src/domain/format/trailer/locateRecords.js +80 -0
- package/dist/packages/core/src/domain/format/trailer/parseRecordIndex.js +28 -0
- package/dist/packages/core/src/domain/format/trailer/readTrailer.js +32 -0
- package/dist/packages/core/src/domain/motion/exposure/ExposureRecord.js +53 -0
- package/dist/packages/core/src/domain/motion/gyro/GyroTrack.js +60 -0
- package/dist/packages/core/src/domain/motion/gyro/medianStep.js +11 -0
- package/dist/packages/core/src/domain/motion/gyro/repairedTimeline.js +101 -0
- package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +112 -0
- package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +146 -0
- package/dist/packages/core/src/domain/motion/orientation/estimateGyroBias.js +64 -0
- package/dist/packages/core/src/domain/motion/orientation/gravity.js +97 -0
- package/dist/packages/core/src/domain/motion/stabilization/Stabilizer.js +13 -0
- package/dist/packages/core/src/domain/motion/stabilization/stabilizers.js +86 -0
- package/dist/packages/core/src/domain/motion/timing/CaptureClock.js +41 -0
- package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +92 -0
- package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +61 -0
- package/dist/packages/core/src/domain/motion/timing/resolveFrameTimes.js +68 -0
- package/dist/packages/core/src/domain/optics/EquidistantModel.js +39 -0
- package/dist/packages/core/src/domain/optics/MeiModel.js +45 -0
- package/dist/packages/core/src/domain/optics/PolynomialModel.js +44 -0
- package/dist/packages/core/src/domain/optics/gainMatch.js +62 -0
- package/dist/packages/core/src/domain/optics/lensGeometry.js +18 -0
- package/dist/packages/core/src/domain/optics/lensPose.js +35 -0
- package/dist/packages/core/src/domain/optics/opticsConstants.js +5 -0
- package/dist/packages/core/src/domain/playback/PlayerState.js +83 -0
- package/dist/packages/core/src/domain/stitching/LensLayout.js +21 -0
- package/dist/packages/core/src/domain/stitching/StitchingSetup.js +78 -0
- package/dist/packages/core/src/domain/stitching/seamDisparity.js +4 -0
- package/dist/packages/core/src/domain/stitching/seamDisparityField.js +5 -0
- package/dist/packages/core/src/domain/stitching/seamJoin.js +16 -0
- package/dist/packages/core/src/domain/stitching/seamMismatch.js +1 -0
- package/dist/packages/core/src/domain/stitching/seamRing.js +4 -0
- package/dist/packages/core/src/domain/stitching/seamStrip.js +26 -0
- package/dist/packages/core/src/domain/view/Framing.js +31 -0
- package/dist/packages/core/src/domain/view/PictureQuality.js +26 -0
- package/dist/packages/core/src/domain/view/ViewMode.js +9 -0
- package/dist/packages/core/src/domain/view/ViewState.js +48 -0
- package/dist/packages/core/src/domain/view/lensTilesView.js +63 -0
- package/dist/packages/core/src/domain/view/magnification.js +103 -0
- package/dist/packages/core/src/domain/view/normalView.js +29 -0
- package/dist/packages/core/src/domain/view/panoramaView.js +99 -0
- package/dist/packages/core/src/domain/view/rectilinear.js +40 -0
- package/dist/packages/core/src/domain/view/screenLayout.js +113 -0
- package/dist/packages/core/src/domain/view/viewGestures.js +98 -0
- package/dist/packages/core/src/domain/view/viewModes.js +14 -0
- package/dist/packages/core/src/index.js +59 -0
- package/dist/packages/core/src/ports/FramePair.js +6 -0
- package/dist/packages/core/src/shared/async/Deferred.js +42 -0
- package/dist/packages/core/src/shared/async/RunStop.js +40 -0
- package/dist/packages/core/src/shared/binary/ByteRange.js +38 -0
- package/dist/packages/core/src/shared/binary/ByteReader.js +77 -0
- package/dist/packages/core/src/shared/errors/GyroViewError.js +81 -0
- package/dist/packages/core/src/shared/errors/reportLater.js +11 -0
- package/dist/packages/core/src/shared/events/Outbox.js +68 -0
- package/dist/packages/core/src/shared/events/TypedEmitter.js +55 -0
- package/dist/packages/core/src/shared/keysOf.js +10 -0
- package/dist/packages/core/src/shared/lazy.js +11 -0
- package/dist/packages/core/src/shared/math/Matrix3.js +93 -0
- package/dist/packages/core/src/shared/math/Quaternion.js +133 -0
- package/dist/packages/core/src/shared/math/Vector3.js +54 -0
- package/dist/packages/core/src/shared/math/clamp.js +9 -0
- package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +157 -0
- package/dist/packages/core/src/shared/text/urlPath.js +31 -0
- package/dist/packages/core/src/shared/text/utf8.js +12 -0
- package/dist/packages/core/src/shared/units/angle.js +22 -0
- package/dist/packages/core/src/shared/units/time.js +13 -0
- package/dist/packages/player/src/PlayerSource.js +15 -0
- package/dist/packages/player/src/browserPlayer.js +18 -0
- package/dist/packages/player/src/choices.js +25 -0
- package/dist/packages/player/src/composition/Disposables.js +31 -0
- package/dist/packages/player/src/composition/browserPorts.js +46 -0
- package/dist/packages/player/src/composition/buildPipeline.js +153 -0
- package/dist/packages/player/src/composition/ensureDecodable.js +63 -0
- package/dist/packages/player/src/composition/openInputs.js +179 -0
- package/dist/packages/player/src/composition/openRecording.js +42 -0
- package/dist/packages/player/src/controls/ChoiceMenu.js +113 -0
- package/dist/packages/player/src/controls/PictureMenus.js +47 -0
- package/dist/packages/player/src/controls/SeekBar.js +125 -0
- package/dist/packages/player/src/controls/SoundControls.js +41 -0
- package/dist/packages/player/src/controls/TransportButtons.js +31 -0
- package/dist/packages/player/src/controls/ViewGestures.js +136 -0
- package/dist/packages/player/src/controls/Wording.js +78 -0
- package/dist/packages/player/src/controls/controlParts.js +36 -0
- package/dist/packages/player/src/controls/controls.js +4 -0
- package/dist/packages/player/src/controls/controlsBar.js +23 -0
- package/dist/packages/player/src/controls/controlsMarkup.js +85 -0
- package/dist/packages/player/src/controls/customControls.js +59 -0
- package/dist/packages/player/src/controls/formatTime.js +19 -0
- package/dist/packages/player/src/controls/icons.js +40 -0
- package/dist/packages/player/src/controls/keyboard.js +118 -0
- package/dist/packages/player/src/controls/messages.js +94 -0
- package/dist/packages/player/src/controls/parseMarkup.js +31 -0
- package/dist/packages/player/src/controls/viewButtons.js +14 -0
- package/dist/packages/player/src/element/ElementLoads.js +119 -0
- package/dist/packages/player/src/element/FullscreenToggle.js +39 -0
- package/dist/packages/player/src/element/GyroViewElement.js +313 -0
- package/dist/packages/player/src/element/IdleWatcher.js +71 -0
- package/dist/packages/player/src/element/TypedEventElement.js +22 -0
- package/dist/packages/player/src/element/accessibleRegion.js +19 -0
- package/dist/packages/player/src/element/attributeNames.js +51 -0
- package/dist/packages/player/src/element/attributes.js +101 -0
- package/dist/packages/player/src/element/defineGyroView.js +21 -0
- package/dist/packages/player/src/element/earlyProperties.js +36 -0
- package/dist/packages/player/src/element/elementSource.js +23 -0
- package/dist/packages/player/src/element/liveSettings.js +138 -0
- package/dist/packages/player/src/element/playbackAttributes.js +53 -0
- package/dist/packages/player/src/element/playerEventMirror.js +33 -0
- package/dist/packages/player/src/element/reflectedProperties.js +78 -0
- package/dist/packages/player/src/element/styles.js +4 -0
- package/dist/packages/player/src/element/template.js +47 -0
- package/dist/packages/player/src/index.js +10 -0
- package/dist/packages/player/src/inspectRecording.js +22 -0
- package/dist/packages/player/src/player/DrawingBufferFit.js +77 -0
- package/dist/packages/player/src/player/FrameLoop.js +40 -0
- package/dist/packages/player/src/player/PictureSettings.js +60 -0
- package/dist/packages/player/src/player/Player.js +377 -0
- package/dist/packages/player/src/player/PlayerEvents.js +28 -0
- package/dist/packages/player/src/player/PlayerPhase.js +21 -0
- package/dist/packages/player/src/player/PlayerSound.js +61 -0
- package/dist/packages/player/src/player/PlayerView.js +113 -0
- package/dist/packages/player/src/player/SessionRelay.js +66 -0
- package/dist/packages/player/src/player/ensureFinite.js +26 -0
- package/dist/packages/player/src/player/loadRecording.js +65 -0
- package/dist/packages/player/src/player/transportEvents.js +29 -0
- package/dist/standalone.js +4919 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GYRO_VIEW_ERROR_CODES, GyroViewError, hasErrorCode, isGyroViewErrorCode } from "./packages/core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import { STABILIZATION_MODES } from "./packages/core/src/domain/motion/stabilization/Stabilizer.js";
|
|
3
|
+
import { PICTURE_QUALITIES } from "./packages/core/src/domain/view/PictureQuality.js";
|
|
4
|
+
import { VIEW_MODES } from "./packages/core/src/domain/view/ViewMode.js";
|
|
5
|
+
import "./packages/core/src/index.js";
|
|
6
|
+
import { attachKeyboard, attachViewGestures } from "./packages/player/src/controls/customControls.js";
|
|
7
|
+
import { Player } from "./packages/player/src/player/Player.js";
|
|
8
|
+
import { createBrowserPlayer } from "./packages/player/src/browserPlayer.js";
|
|
9
|
+
import { inspectRecording } from "./packages/player/src/inspectRecording.js";
|
|
10
|
+
import { GyroViewElement } from "./packages/player/src/element/GyroViewElement.js";
|
|
11
|
+
import { GYRO_VIEW_TAG, defineGyroView } from "./packages/player/src/element/defineGyroView.js";
|
|
12
|
+
import "./packages/player/src/index.js";
|
|
13
|
+
export { GYRO_VIEW_ERROR_CODES, GYRO_VIEW_TAG, GyroViewElement, GyroViewError, PICTURE_QUALITIES, Player, STABILIZATION_MODES, VIEW_MODES, attachKeyboard, attachViewGestures, createBrowserPlayer, defineGyroView, hasErrorCode, inspectRecording, isGyroViewErrorCode };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import "../../../core/src/index.js";
|
|
3
|
+
//#region ../../packages/adapters/blob/src/BlobRandomAccessSource.ts
|
|
4
|
+
/**
|
|
5
|
+
* RandomAccessSource over a `Blob` (a `File` from a picker or a drop). Each read slices the
|
|
6
|
+
* blob, which browsers serve from disk without loading the whole file, so multi-gigabyte
|
|
7
|
+
* recordings play from local storage the same way they play from a URL.
|
|
8
|
+
*/
|
|
9
|
+
var BlobRandomAccessSource = class {
|
|
10
|
+
blob;
|
|
11
|
+
constructor(blob) {
|
|
12
|
+
this.blob = blob;
|
|
13
|
+
}
|
|
14
|
+
size() {
|
|
15
|
+
return Promise.resolve(this.blob.size);
|
|
16
|
+
}
|
|
17
|
+
async read(range) {
|
|
18
|
+
range.ensureWithin(this.blob.size, "blob");
|
|
19
|
+
if (range.length === 0) return /* @__PURE__ */ new Uint8Array();
|
|
20
|
+
const bytes = await this.bytesOf(this.blob.slice(range.offset, range.end));
|
|
21
|
+
if (bytes.byteLength !== range.length) throw new GyroViewError("source-truncated", `the blob yielded ${bytes.byteLength} bytes for a ${range.length}-byte range at ${range.offset}; the file may have changed on disk`);
|
|
22
|
+
return bytes;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reading is what fails, with a `NotReadableError`, when a picked file has changed or gone on
|
|
26
|
+
* disk; slicing only clamps its arguments.
|
|
27
|
+
*/
|
|
28
|
+
async bytesOf(slice) {
|
|
29
|
+
try {
|
|
30
|
+
return new Uint8Array(await slice.arrayBuffer());
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new GyroViewError("source-unreadable", "the blob could not be read", { cause: error });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { BlobRandomAccessSource };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { GyroViewError, hasErrorCode, isAbortError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import "../../../core/src/index.js";
|
|
3
|
+
import { EXPOSED_HEADERS_ADVICE, FIRST_BYTE_RANGE, discardBody, httpRequest, plainHttpRequest, withAbortSignal } from "./httpRequest.js";
|
|
4
|
+
//#region ../../packages/adapters/fetch/src/HttpRangeSource.ts
|
|
5
|
+
var HTTP_OK = 200;
|
|
6
|
+
var HTTP_PARTIAL_CONTENT = 206;
|
|
7
|
+
var HTTP_SERVER_ERROR = 500;
|
|
8
|
+
var DEFAULT_RETRY_DELAYS_MS = [250, 1e3];
|
|
9
|
+
/**
|
|
10
|
+
* The failures on the way, which asking again may cure; any other fails the read at once. They
|
|
11
|
+
* are `source-unreadable` errors like any other, so a caller that gets one after the retries
|
|
12
|
+
* cannot tell, and need not: only the retry loop asks.
|
|
13
|
+
*/
|
|
14
|
+
var passingFailures = /* @__PURE__ */ new WeakSet();
|
|
15
|
+
function passing(failure) {
|
|
16
|
+
passingFailures.add(failure);
|
|
17
|
+
return failure;
|
|
18
|
+
}
|
|
19
|
+
var CONTENT_RANGE_TOTAL = /\/(\d+)$/u;
|
|
20
|
+
/**
|
|
21
|
+
* RandomAccessSource over HTTP. The server must answer `Range` requests with 206 and, for
|
|
22
|
+
* cross-origin use, send CORS headers that expose `Content-Range`; both are hard requirements
|
|
23
|
+
* of playing a remote recording and are reported with distinct error codes.
|
|
24
|
+
*/
|
|
25
|
+
var HttpRangeSource = class {
|
|
26
|
+
url;
|
|
27
|
+
options;
|
|
28
|
+
retryDelaysMs;
|
|
29
|
+
sizePromise;
|
|
30
|
+
/**
|
|
31
|
+
* A range has come through: CORS is proven for this source, so a request that fails from here
|
|
32
|
+
* on failed on the way, whatever the diagnosis says of an error page without CORS headers.
|
|
33
|
+
*/
|
|
34
|
+
hasReadRange = false;
|
|
35
|
+
/**
|
|
36
|
+
* `signal` ends every request the source makes, with the host's own `requestInit` signal.
|
|
37
|
+
*/
|
|
38
|
+
constructor(url, options = {}, signal) {
|
|
39
|
+
this.url = url;
|
|
40
|
+
this.options = signal ? withAbortSignal(options, signal) : options;
|
|
41
|
+
this.retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One HEAD request (or a one-byte range where HEAD is refused or gives no length), cached for
|
|
45
|
+
* the lifetime of the source once it succeeded; a failed lookup is retried on the next call.
|
|
46
|
+
*/
|
|
47
|
+
size() {
|
|
48
|
+
this.sizePromise ??= this.rememberSize();
|
|
49
|
+
return this.sizePromise;
|
|
50
|
+
}
|
|
51
|
+
async read(range) {
|
|
52
|
+
await this.ensureFits(range);
|
|
53
|
+
return range.length === 0 ? /* @__PURE__ */ new Uint8Array() : this.readAskingAgain(range);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A range that failed on the way is asked for again after a short wait, a few times: one
|
|
57
|
+
* dropped connection must not end a long playback. What asking again cannot change (an abort,
|
|
58
|
+
* a refusal, a server ignoring ranges, CORS) fails at once.
|
|
59
|
+
*/
|
|
60
|
+
async readAskingAgain(range) {
|
|
61
|
+
for (const delayMs of this.retryDelaysMs) {
|
|
62
|
+
try {
|
|
63
|
+
return await this.readOnce(range);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!(error instanceof GyroViewError && passingFailures.has(error))) throw error;
|
|
66
|
+
}
|
|
67
|
+
await wait(delayMs, this.options.requestInit?.signal);
|
|
68
|
+
}
|
|
69
|
+
return this.readOnce(range);
|
|
70
|
+
}
|
|
71
|
+
async readOnce(range) {
|
|
72
|
+
const response = await this.requestRange(range);
|
|
73
|
+
if (response.status !== HTTP_PARTIAL_CONTENT) {
|
|
74
|
+
discardBody(response);
|
|
75
|
+
throw this.refusalOf(response.status);
|
|
76
|
+
}
|
|
77
|
+
this.hasReadRange = true;
|
|
78
|
+
const bytes = await this.bodyOf(response, range);
|
|
79
|
+
if (bytes.byteLength !== range.length) throw new GyroViewError("source-truncated", `${this.url} returned ${bytes.byteLength} bytes for a ${range.length}-byte range at ${range.offset}`);
|
|
80
|
+
return bytes;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A request that did not get through for want of a network is worth another try; one CORS
|
|
84
|
+
* refused before any range came through is not.
|
|
85
|
+
*/
|
|
86
|
+
async requestRange(range) {
|
|
87
|
+
const headers = { Range: `bytes=${range.offset}-${range.end - 1}` };
|
|
88
|
+
try {
|
|
89
|
+
return this.hasReadRange ? await plainHttpRequest(this.url, {
|
|
90
|
+
method: "GET",
|
|
91
|
+
headers
|
|
92
|
+
}, this.options) : await this.request("GET", headers);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw this.hasReadRange ? this.passingOnceAnswered(error) : passingIfUnreachable(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
passingOnceAnswered(error) {
|
|
98
|
+
const message = `${this.url} could not be reached for a byte range`;
|
|
99
|
+
return isAbortError(error) ? error : passing(new GyroViewError("source-unreadable", message, { cause: error }));
|
|
100
|
+
}
|
|
101
|
+
refusalOf(status) {
|
|
102
|
+
if (status === HTTP_OK) return new GyroViewError("range-unsupported", `${this.url} ignores Range requests (answered 200 to a byte range)`);
|
|
103
|
+
const message = `${this.url} answered ${status} to a byte range`;
|
|
104
|
+
return status >= HTTP_SERVER_ERROR ? passing(new GyroViewError("source-unreadable", message)) : new GyroViewError("source-unreadable", message);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The bytes of a range, which a dropped connection can break off after the headers: the most
|
|
108
|
+
* common way a network fails during long playback, so it is `source-unreadable` as a refused
|
|
109
|
+
* request is, not a bug or a bad file, and worth another try.
|
|
110
|
+
*/
|
|
111
|
+
async bodyOf(response, range) {
|
|
112
|
+
try {
|
|
113
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isAbortError(error)) throw error;
|
|
116
|
+
throw passing(new GyroViewError("source-unreadable", `${this.url} broke off the ${range.length}-byte range at ${range.offset}`, { cause: error }));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Servers clamp out-of-range requests instead of failing them; the port contract wants a
|
|
121
|
+
* typed error, so the range is checked against the (cached) size first.
|
|
122
|
+
*/
|
|
123
|
+
async ensureFits(range) {
|
|
124
|
+
range.ensureWithin(await this.size(), `resource ${this.url}`);
|
|
125
|
+
}
|
|
126
|
+
async rememberSize() {
|
|
127
|
+
try {
|
|
128
|
+
return await this.fetchSize();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.sizePromise = void 0;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async fetchSize() {
|
|
135
|
+
const response = await this.request("HEAD");
|
|
136
|
+
discardBody(response);
|
|
137
|
+
if (!response.ok) return this.sizeFromContentRange();
|
|
138
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
139
|
+
return Number.isSafeInteger(contentLength) && contentLength > 0 ? contentLength : this.sizeFromContentRange();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Some servers omit Content-Length on HEAD or refuse HEAD; a one-byte range reveals the total.
|
|
143
|
+
*/
|
|
144
|
+
async sizeFromContentRange() {
|
|
145
|
+
const response = await this.request("GET", { Range: FIRST_BYTE_RANGE });
|
|
146
|
+
discardBody(response);
|
|
147
|
+
if (response.status !== HTTP_PARTIAL_CONTENT) throw new GyroViewError("source-unreadable", `${this.url} answered ${response.status} to a byte range; cannot determine the file size`);
|
|
148
|
+
this.hasReadRange = true;
|
|
149
|
+
const contentRange = response.headers.get("content-range");
|
|
150
|
+
if (contentRange === null && response.type === "cors") throw this.hiddenContentRange();
|
|
151
|
+
const total = CONTENT_RANGE_TOTAL.exec(contentRange ?? "")?.[1];
|
|
152
|
+
if (total === void 0) throw new GyroViewError("source-unreadable", `${this.url} reports neither Content-Length nor Content-Range; cannot determine the file size`);
|
|
153
|
+
return Number(total);
|
|
154
|
+
}
|
|
155
|
+
hiddenContentRange() {
|
|
156
|
+
return new GyroViewError("cors", `${this.url} answered a byte range but hides its Content-Range from this origin; add ${EXPOSED_HEADERS_ADVICE}`);
|
|
157
|
+
}
|
|
158
|
+
request(method, headers) {
|
|
159
|
+
return httpRequest(this.url, {
|
|
160
|
+
method,
|
|
161
|
+
...headers && { headers }
|
|
162
|
+
}, this.options);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* A pause before asking again, which an abort of the source's requests ends at once.
|
|
167
|
+
*/
|
|
168
|
+
function wait(ms, signal) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
const settled = new AbortController();
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
settled.abort();
|
|
174
|
+
resolve();
|
|
175
|
+
}, ms);
|
|
176
|
+
const onAbort = () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
reject(abortErrorOf(signal));
|
|
179
|
+
};
|
|
180
|
+
signal?.addEventListener("abort", onAbort, {
|
|
181
|
+
once: true,
|
|
182
|
+
signal: settled.signal
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function passingIfUnreachable(error) {
|
|
187
|
+
return hasErrorCode(error, "source-unreadable") && error instanceof Error ? passing(new GyroViewError("source-unreadable", error.message, { cause: error.cause })) : error;
|
|
188
|
+
}
|
|
189
|
+
function abortErrorOf(signal) {
|
|
190
|
+
const reason = signal?.reason;
|
|
191
|
+
return reason instanceof Error ? reason : new DOMException("the read was aborted", "AbortError");
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
export { HttpRangeSource };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FIRST_BYTE_RANGE, discardBody, plainHttpRequest } from "./httpRequest.js";
|
|
2
|
+
//#region ../../packages/adapters/fetch/src/HttpResourceLocator.ts
|
|
3
|
+
/**
|
|
4
|
+
* How a server refuses HEAD: a 405, or a 403 from a URL signed for GET alone. The first byte
|
|
5
|
+
* then answers what HEAD would.
|
|
6
|
+
*/
|
|
7
|
+
var REFUSED_HEAD = /* @__PURE__ */ new Set([405, 403]);
|
|
8
|
+
/**
|
|
9
|
+
* ResourceLocator over HTTP: one HEAD request, or a one-byte GET when the server refuses HEAD.
|
|
10
|
+
* Every failure, including a missing CORS header, means "not available"; the file looked for is
|
|
11
|
+
* optional, so nothing here throws, and no request is spent explaining a failure.
|
|
12
|
+
*/
|
|
13
|
+
var HttpResourceLocator = class {
|
|
14
|
+
options;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
async exists(url) {
|
|
19
|
+
try {
|
|
20
|
+
const head = await plainHttpRequest(url, { method: "HEAD" }, this.options);
|
|
21
|
+
discardBody(head);
|
|
22
|
+
if (!REFUSED_HEAD.has(head.status)) return head.ok;
|
|
23
|
+
const firstByte = await plainHttpRequest(url, {
|
|
24
|
+
method: "GET",
|
|
25
|
+
headers: { Range: FIRST_BYTE_RANGE }
|
|
26
|
+
}, this.options);
|
|
27
|
+
discardBody(firstByte);
|
|
28
|
+
return firstByte.ok;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { HttpResourceLocator };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { GyroViewError, isAbortError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import "../../../core/src/index.js";
|
|
3
|
+
//#region ../../packages/adapters/fetch/src/httpRequest.ts
|
|
4
|
+
/**
|
|
5
|
+
* The Range header value that asks for the first byte only: enough to learn whether a resource
|
|
6
|
+
* is there, or its total size from Content-Range.
|
|
7
|
+
*/
|
|
8
|
+
var FIRST_BYTE_RANGE = "bytes=0-0";
|
|
9
|
+
/**
|
|
10
|
+
* The response headers a cross-origin page must be let read, as the fix a CORS failure names.
|
|
11
|
+
*/
|
|
12
|
+
var EXPOSED_HEADERS_ADVICE = "Access-Control-Expose-Headers: Content-Range, Content-Length, Accept-Ranges";
|
|
13
|
+
/**
|
|
14
|
+
* Browsers keep byte ranges of one URL as a sparse cache entry and revalidate them with
|
|
15
|
+
* conditional requests. A 304 to such a request makes the browser answer from that entry, and
|
|
16
|
+
* Chrome has been seen to hand back an empty body for a range it believed it held (ADR 0013).
|
|
17
|
+
* The demuxer caches what it needs itself, so nothing is lost by leaving the cache out.
|
|
18
|
+
*/
|
|
19
|
+
var CACHE_MODE = "no-store";
|
|
20
|
+
/**
|
|
21
|
+
* One request with the shared settings merged in, failures explained: a request that fails
|
|
22
|
+
* outright is told apart from one the browser blocked for want of CORS headers (the latter is
|
|
23
|
+
* `cors`, the former `source-unreadable`). A caller's abort is passed on as it is.
|
|
24
|
+
*/
|
|
25
|
+
async function httpRequest(url, request, options) {
|
|
26
|
+
try {
|
|
27
|
+
return await plainHttpRequest(url, request, options);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (isAbortError(error)) throw error;
|
|
30
|
+
throw await diagnoseFailure(url, options, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One request with the shared settings merged in and its failure left as the platform reported
|
|
35
|
+
* it, for callers to whom any failure means the same.
|
|
36
|
+
*/
|
|
37
|
+
function plainHttpRequest(url, request, options) {
|
|
38
|
+
return (options.fetch ?? fetch)(url, {
|
|
39
|
+
cache: CACHE_MODE,
|
|
40
|
+
...options.requestInit,
|
|
41
|
+
method: request.method,
|
|
42
|
+
headers: {
|
|
43
|
+
...headersOf(options.requestInit),
|
|
44
|
+
...request.headers
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The options with `signal` ending their requests too, beside any signal the host gave: made once
|
|
50
|
+
* per source, so the listeners it adds do not pile up per request.
|
|
51
|
+
*/
|
|
52
|
+
function withAbortSignal(options, signal) {
|
|
53
|
+
const hostSignal = options.requestInit?.signal ?? void 0;
|
|
54
|
+
const either = hostSignal ? eitherSignal(hostSignal, signal) : signal;
|
|
55
|
+
return {
|
|
56
|
+
...options,
|
|
57
|
+
requestInit: {
|
|
58
|
+
...options.requestInit,
|
|
59
|
+
signal: either
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Fires when either does. `AbortSignal.any` would do, but iOS Safari has it only from 17.4.
|
|
65
|
+
* Whichever fires takes both listeners along, so a long-lived host signal keeps none.
|
|
66
|
+
*/
|
|
67
|
+
function eitherSignal(first, second) {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
for (const signal of [first, second]) {
|
|
70
|
+
if (signal.aborted) controller.abort(signal.reason);
|
|
71
|
+
signal.addEventListener("abort", () => {
|
|
72
|
+
controller.abort(signal.reason);
|
|
73
|
+
}, {
|
|
74
|
+
once: true,
|
|
75
|
+
signal: controller.signal
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return controller.signal;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Browsers report a CORS refusal and a network failure alike, as a rejected fetch. A request
|
|
82
|
+
* that asks for no CORS answer at all (an opaque response) still succeeds when the server is
|
|
83
|
+
* reachable, so its outcome tells the two apart.
|
|
84
|
+
*/
|
|
85
|
+
async function diagnoseFailure(url, options, cause) {
|
|
86
|
+
return await isReachableWithoutCors(url, options) ? new GyroViewError("cors", `${url} answered, but its server does not allow this page's origin to read it; add CORS headers (Access-Control-Allow-Origin, ${EXPOSED_HEADERS_ADVICE})`, { cause }) : new GyroViewError("source-unreadable", `${url} could not be fetched; check the URL and the network`, { cause });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The probe ends with the request it diagnoses: an abort of the load ends it too.
|
|
90
|
+
*/
|
|
91
|
+
async function isReachableWithoutCors(url, options) {
|
|
92
|
+
const doFetch = options.fetch ?? fetch;
|
|
93
|
+
const signal = options.requestInit?.signal ?? null;
|
|
94
|
+
try {
|
|
95
|
+
discardBody(await doFetch(url, {
|
|
96
|
+
method: "HEAD",
|
|
97
|
+
mode: "no-cors",
|
|
98
|
+
signal
|
|
99
|
+
}));
|
|
100
|
+
return true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (isAbortError(error)) throw error;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Lets the browser stop streaming a body that will not be read, for example a whole file
|
|
108
|
+
* answered to a byte range.
|
|
109
|
+
*/
|
|
110
|
+
function discardBody(response) {
|
|
111
|
+
cancelBody(response);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A body the request's abort already ended refuses the cancel with that abort: there is nothing
|
|
115
|
+
* left to stop, and the abort reaches the caller through the request itself.
|
|
116
|
+
*/
|
|
117
|
+
async function cancelBody(response) {
|
|
118
|
+
try {
|
|
119
|
+
await response.body?.cancel();
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
function headersOf(init) {
|
|
123
|
+
return Object.fromEntries(new Headers(init?.headers).entries());
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { EXPOSED_HEADERS_ADVICE, FIRST_BYTE_RANGE, discardBody, httpRequest, plainHttpRequest, withAbortSignal };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import { seconds } from "../../../core/src/shared/units/time.js";
|
|
3
|
+
import "../../../core/src/index.js";
|
|
4
|
+
import { SegmentChannel } from "./SegmentChannel.js";
|
|
5
|
+
import { EncodedAudioPacketSource, EncodedPacketSink, Mp4OutputFormat, NullTarget, Output } from "mediabunny";
|
|
6
|
+
//#region ../../packages/adapters/mediabunny/src/MediabunnyAudioSegments.ts
|
|
7
|
+
/**
|
|
8
|
+
* Seconds of audio per media segment: short enough that a seek has sound quickly, long enough
|
|
9
|
+
* that appends stay cheap.
|
|
10
|
+
*/
|
|
11
|
+
var FRAGMENT_DURATION_SECONDS = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Segments (each fragment is two: `moof` and `mdat`) that may wait for the consumer before
|
|
14
|
+
* re-packaging pauses.
|
|
15
|
+
*/
|
|
16
|
+
var SEGMENTS_AHEAD = 4;
|
|
17
|
+
/**
|
|
18
|
+
* An audio track's packets re-packaged, unchanged, into fragmented MP4 with mediabunny.
|
|
19
|
+
* Segments are taken from the muxer's box callbacks rather than from its byte stream: that
|
|
20
|
+
* yields whole `ftyp`/`moov`/`moof`/`mdat` boxes and leaves out the `mfra` index the muxer
|
|
21
|
+
* appends at the end, which Media Source Extensions do not accept. Fragmented output keeps the
|
|
22
|
+
* track's own timestamps, so a run started mid-track lands at its true time.
|
|
23
|
+
*/
|
|
24
|
+
var MediabunnyAudioSegments = class MediabunnyAudioSegments {
|
|
25
|
+
parts;
|
|
26
|
+
duration;
|
|
27
|
+
constructor(parts, duration) {
|
|
28
|
+
this.parts = parts;
|
|
29
|
+
this.duration = duration;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rejects with `codec-unsupported`, naming the track, when mediabunny cannot re-package it: the
|
|
33
|
+
* picture then plays without sound rather than not at all.
|
|
34
|
+
*/
|
|
35
|
+
static async open(track, trackIndex) {
|
|
36
|
+
const [codec, decoderConfig, duration] = await Promise.all([
|
|
37
|
+
track.getCodec(),
|
|
38
|
+
track.getDecoderConfig(),
|
|
39
|
+
track.computeDuration()
|
|
40
|
+
]);
|
|
41
|
+
if (codec === null || decoderConfig === null) {
|
|
42
|
+
const named = await track.getCodecParameterString() ?? "an unknown codec";
|
|
43
|
+
throw new GyroViewError("codec-unsupported", `audio track ${trackIndex} (${named}) cannot be re-packaged for playback`);
|
|
44
|
+
}
|
|
45
|
+
return new MediabunnyAudioSegments({
|
|
46
|
+
track,
|
|
47
|
+
codec,
|
|
48
|
+
decoderConfig
|
|
49
|
+
}, seconds(duration));
|
|
50
|
+
}
|
|
51
|
+
get mimeType() {
|
|
52
|
+
return `audio/mp4; codecs="${this.parts.decoderConfig.codec}"`;
|
|
53
|
+
}
|
|
54
|
+
segmentsFrom(from) {
|
|
55
|
+
return remux(this.parts, from);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
async function* remux(parts, from) {
|
|
59
|
+
const channel = new SegmentChannel(SEGMENTS_AHEAD);
|
|
60
|
+
const output = new Output({
|
|
61
|
+
format: fragmentedMp4Into(channel),
|
|
62
|
+
target: new NullTarget()
|
|
63
|
+
});
|
|
64
|
+
const source = new EncodedAudioPacketSource(parts.codec);
|
|
65
|
+
output.addAudioTrack(source);
|
|
66
|
+
const production = produce({
|
|
67
|
+
parts,
|
|
68
|
+
from,
|
|
69
|
+
output,
|
|
70
|
+
source,
|
|
71
|
+
channel
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
yield* channel.segments();
|
|
75
|
+
} finally {
|
|
76
|
+
channel.close();
|
|
77
|
+
releaseAfter(production, output);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Frees the muxer once the producer has noticed the closed channel, without making a consumer
|
|
82
|
+
* that left early (a seek) wait for a packet read still in flight.
|
|
83
|
+
*/
|
|
84
|
+
async function releaseAfter(production, output) {
|
|
85
|
+
await production;
|
|
86
|
+
if (output.state === "finalized") return;
|
|
87
|
+
try {
|
|
88
|
+
await output.cancel();
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Feeds packets to the muxer at the consumer's pace; every failure ends up in the channel.
|
|
93
|
+
*/
|
|
94
|
+
async function produce(run) {
|
|
95
|
+
try {
|
|
96
|
+
await run.output.start();
|
|
97
|
+
let meta = { decoderConfig: run.parts.decoderConfig };
|
|
98
|
+
for await (const packet of packetsFrom(run.parts.track, run.from)) {
|
|
99
|
+
await run.channel.waitForRoom();
|
|
100
|
+
if (run.channel.isClosed) return;
|
|
101
|
+
await run.source.add(packet, meta);
|
|
102
|
+
meta = void 0;
|
|
103
|
+
}
|
|
104
|
+
await run.output.finalize();
|
|
105
|
+
run.channel.close();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
run.channel.fail(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function fragmentedMp4Into(channel) {
|
|
111
|
+
const forward = (data) => {
|
|
112
|
+
channel.push(new Uint8Array(data));
|
|
113
|
+
};
|
|
114
|
+
return new Mp4OutputFormat({
|
|
115
|
+
fastStart: "fragmented",
|
|
116
|
+
minimumFragmentDuration: FRAGMENT_DURATION_SECONDS,
|
|
117
|
+
onFtyp: forward,
|
|
118
|
+
onMoov: forward,
|
|
119
|
+
onMoof: forward,
|
|
120
|
+
onMdat: forward
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Packets in decode order from the one playing at `time` (or the first one after it) to the end.
|
|
125
|
+
*/
|
|
126
|
+
async function* packetsFrom(track, time) {
|
|
127
|
+
const sink = new EncodedPacketSink(track);
|
|
128
|
+
const start = await sink.getPacket(time) ?? await sink.getFirstPacket();
|
|
129
|
+
if (start === null) return;
|
|
130
|
+
yield* sink.packets(start);
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { MediabunnyAudioSegments };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { GyroViewError, isAbortError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import { seconds } from "../../../core/src/shared/units/time.js";
|
|
3
|
+
import { ByteRange } from "../../../core/src/shared/binary/ByteRange.js";
|
|
4
|
+
import "../../../core/src/index.js";
|
|
5
|
+
import { MediabunnyAudioSegments } from "./MediabunnyAudioSegments.js";
|
|
6
|
+
import { MediabunnyVideoTrackReader } from "./MediabunnyVideoTrackReader.js";
|
|
7
|
+
import { ALL_FORMATS, CustomSource, Input } from "mediabunny";
|
|
8
|
+
//#region ../../packages/adapters/mediabunny/src/MediabunnyDemuxer.ts
|
|
9
|
+
/**
|
|
10
|
+
* Demuxer over mediabunny. Bytes flow through the core's RandomAccessSource port (one HTTP
|
|
11
|
+
* client, one cache), presented to mediabunny as a CustomSource with network prefetching.
|
|
12
|
+
*/
|
|
13
|
+
var MediabunnyDemuxer = class {
|
|
14
|
+
async open(source, name) {
|
|
15
|
+
const input = new Input({
|
|
16
|
+
formats: ALL_FORMATS,
|
|
17
|
+
source: new CustomSource({
|
|
18
|
+
getSize: () => source.size(),
|
|
19
|
+
read: (start, end) => source.read(ByteRange.of(start, end - start)),
|
|
20
|
+
prefetchProfile: "network",
|
|
21
|
+
handleUnhandledError: ignoreFailedPrefetch
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
return await describeInput(input, name);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
input.dispose();
|
|
28
|
+
throw failureOf(error, name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The source's own failures and a caller's abort pass as they are; anything else the demuxer
|
|
34
|
+
* throws means the bytes are no media file it can read.
|
|
35
|
+
*/
|
|
36
|
+
function failureOf(error, name) {
|
|
37
|
+
const isPassedOn = isAbortError(error) || error instanceof GyroViewError;
|
|
38
|
+
const message = `${name ?? "the input"} is not a readable media file`;
|
|
39
|
+
return isPassedOn ? error : new GyroViewError("unsupported-container", message, { cause: error });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A read ahead that fails while no read waits for it: left unhandled it would reach the host
|
|
43
|
+
* page's error reporting. The read that needs those bytes asks again and fails through the port.
|
|
44
|
+
*/
|
|
45
|
+
function ignoreFailedPrefetch() {}
|
|
46
|
+
async function describeInput(input, name) {
|
|
47
|
+
const [videoTracks, audioTracks, duration] = await Promise.all([
|
|
48
|
+
input.getVideoTracks(),
|
|
49
|
+
input.getAudioTracks(),
|
|
50
|
+
input.computeDuration()
|
|
51
|
+
]);
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
duration: seconds(duration),
|
|
55
|
+
videoTracks: await Promise.all(videoTracks.map((track, trackIndex) => MediabunnyVideoTrackReader.open(track, trackIndex))),
|
|
56
|
+
audioTracks: audioTracks.map((track, trackIndex) => ({ openSegments: () => MediabunnyAudioSegments.open(track, trackIndex) })),
|
|
57
|
+
dispose: () => {
|
|
58
|
+
input.dispose();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { MediabunnyDemuxer };
|